---
title: Language of Physics
format:
html:
theme: flatly
grid:
body-width: 1000px # Domyślnie jest to ok. 800-900px
margin-width: 250px
toc: true
toc-depth: 3
highlight-style: tango
code-line-numbers: true
code-fold: true
code-summary: "Show the code"
code-tools: true
code-block-bg: "rgba(42, 174, 42, 0.02)"
code-block-border-left: "#2aae2a"
code-language-label: true
css: styles.css
math: mathjax
self-contained: true
other-links:
- text: Main page
href: https://dchorazkiewicz.github.io/Mathematics_Physics_Lectures
---
## Introduction
Physics often describes the universe and its phenomena using the language of mathematics, where the Cartesian 3D space serves as the natural stage for physical events. This spatial representation, defined by three orthogonal axes $x$, $y$, and $z$, is the starting point for our exploration. By reducing the dimensionality to 2D, we simplify the analysis, enabling a focused study of structures that arise in such spaces. This reduction is not merely an abstraction but also a practical tool for understanding the behaviors and interactions within physical systems.
## Weather Data Visualization
Interactive weather maps serve as an excellent introduction to the fundamental ideas required to study physics. If one can interpret these maps, they will find it easier to grasp the basic concepts of scalar and vector fields, which are central to understanding physical systems.
On such maps, temperature is represented using a color gradient, where each color corresponds to a specific value at a given point in space. This visualization aligns with the definition of a **scalar field**, which assigns a single numerical value to each point in space.
Similarly, wind patterns are illustrated using moving line segments, where their length and direction represent the speed and direction of the wind at various points. These segments directly depict **vector fields**, as they assign a vector—defined by both magnitude and direction—to each point in space. Since the magnitude is expressed as a numerical value, the background color can be used to represent the magnitude of the vector field.
```{=html}
<iframe width="650" height="450" src="https://embed.windy.com/embed2.html" frameborder="0" data-external="1"></iframe>
```
[www.windy.com](https://www.windy.com/)
Having this background, we can now delve into the mathematical representations of scalar and vector fields in 2D space. Let us use more formal definitions to understand these concepts better.
## Scalar Fields
A scalar field associates a single real value to every point in a given space. In 2D, this can be represented as a function $T(x, y)$, where $T$ could represent a physical property like temperature.
### Example: Temperature on a surface
Consider a simple model where the temperature of a surface varies with both position and time:
$$
T(x, y) = 5 \sin(x) \cos(y)
$$
Below is a Python code of this scalar field:
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Scalar field definition
def scalar_field(x, y):
return 5 * np.sin(x) * np.cos(y)
# Generate grid points
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
x, y = np.meshgrid(x, y)
# Compute scalar field values
T = scalar_field(x, y)
# Plot the scalar field
plt.figure(figsize=(8, 6))
plt.imshow(T, extent=[-5, 5, -5, 5], origin='lower', cmap="coolwarm")
plt.colorbar(label="Temperature")
plt.xlabel("X-axis")
plt.ylabel("Y-axis")
plt.title("Temperature Distribution on a Surface")
plt.show()
```
Above example defines a scalar field that do not depend on time. As we can see on weather maps, scalar fields can also be time-dependent, where the value of the field changes with time.
To define a time-dependent scalar field, we can modify the previous example by adding explicit time dependence:
$$
T(x, y, t) = 5 \sin(x) \cos(y) e^{-t}
$$
Now the temperature at each point $(x, y)$ decays exponentially with time. To visualize this field, we can use the following Python
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Scalar field definition
def scalar_field(x, y, t):
return 5 * np.sin(x) * np.cos(y) * np.exp(-t)
# Generate grid points
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
x, y = np.meshgrid(x, y)
# Compute scalar field values for 4 time steps
T = np.zeros((100, 100, 4))
for i in range(4):
T[:, :, i] = scalar_field(x, y, i)
# Determine common color scale
vmin = np.min(T)
vmax = np.max(T)
# Plot the scalar field for 4 time steps
fig, axs = plt.subplots(2, 2, figsize=(12, 12))
# Store the last image for the colorbar
im = None
for i, ax in enumerate(axs.flat):
ax.grid(False) # Explicitly disable gridlines
im = ax.imshow(T[:, :, i], extent=[-5, 5, -5, 5], origin='lower', cmap="coolwarm", vmin=vmin, vmax=vmax)
ax.set_title(f"Temperature Distribution at t={i}")
ax.set_xlabel("X-axis")
ax.set_ylabel("Y-axis")
# Adjust layout to fit a colorbar outside the plots
fig.subplots_adjust(right=0.85)
cbar_ax = fig.add_axes([0.88, 0.15, 0.02, 0.7]) # Define a new axis for the colorbar
cbar = fig.colorbar(im, cax=cbar_ax)
cbar.set_label("Temperature")
plt.show()
```
or we can simulate this in an html animation:
```{=html}
<div style="font-family: sans-serif; text-align: center; margin: 20px 0; padding: 20px; border: 1px solid #e0e0e0; border-radius: 8px; background-color: #f9f9f9;">
<div style="margin-bottom: 15px;">
<label for="timeSlider" style="font-weight: bold;">Czas t: <span id="timeDisplay">0.00</span></label>
<br>
<input type="range" id="timeSlider" min="0" max="5" step="0.05" value="0" style="width: 80%; max-width: 400px; margin-top: 10px;">
</div>
<canvas id="scalarFieldCanvas" width="400" height="400" style="border: 1px solid #ccc; background-color: white;"></canvas>
<script>
(function() {
const canvas = document.getElementById('scalarFieldCanvas');
const ctx = canvas.getContext('2d');
const slider = document.getElementById('timeSlider');
const timeDisplay = document.getElementById('timeDisplay');
const width = canvas.width;
const height = canvas.height;
// Konfiguracja zakresu
const xMin = -5, xMax = 5;
const yMin = -5, yMax = 5;
// Maksymalna wartość amplitudy dla t=0 (do stałej skali kolorów)
const globalMax = 5.0;
// Funkcja pola skalarnego
function scalarField(x, y, t) {
return 5 * Math.sin(x) * Math.cos(y) * Math.exp(-t);
}
// Funkcja mapująca wartość na kolor (Blue -> White -> Red)
function valueToColor(val) {
// Normalizacja wartości do zakresu [-1, 1] względem globalnego maksimum
let norm = val / globalMax;
let r, g, b;
if (norm > 0) {
// Pozytywne - od białego do czerwonego
// norm = 0 -> biały (255,255,255), norm = 1 -> czerwony (255,0,0)
const intensity = Math.floor(255 * (1 - Math.min(norm, 1)));
r = 255;
g = intensity;
b = intensity;
} else {
// Negatywne - od białego do niebieskiego
// norm = 0 -> biały, norm = -1 -> niebieski (0,0,255)
const intensity = Math.floor(255 * (1 - Math.min(Math.abs(norm), 1)));
r = intensity;
g = intensity;
b = 255;
}
return `rgb(${r},${g},${b})`;
}
function draw(t) {
// Tworzymy obraz piksel po pikselu dla wydajności przy animacji
const imageData = ctx.createImageData(width, height);
const data = imageData.data;
for (let py = 0; py < height; py++) {
// Mapowanie piksela na współrzędną Y (odwracamy oś Y, aby góra była dodatnia)
const y = yMax - (py / height) * (yMax - yMin);
for (let px = 0; px < width; px++) {
// Mapowanie piksela na współrzędną X
const x = xMin + (px / width) * (xMax - xMin);
const val = scalarField(x, y, t);
// Obliczanie koloru
// Musimy to zrobić ręcznie dla imageData
let norm = val / globalMax;
let r, g, b;
if (norm > 0) {
let intensity = 255 * (1 - Math.min(norm, 1));
r = 255;
g = intensity;
b = intensity;
} else {
let intensity = 255 * (1 - Math.min(Math.abs(norm), 1));
r = intensity;
g = intensity;
b = 255;
}
const index = (py * width + px) * 4;
data[index] = r; // Red
data[index + 1] = g; // Green
data[index + 2] = b; // Blue
data[index + 3] = 255; // Alpha
}
}
ctx.putImageData(imageData, 0, 0);
}
// Obsługa zdarzeń
slider.addEventListener('input', function() {
const t = parseFloat(this.value);
timeDisplay.innerText = t.toFixed(2);
// Używamy requestAnimationFrame dla płynności
window.requestAnimationFrame(() => draw(t));
});
// Pierwsze rysowanie
draw(0);
})();
</script>
</div>
```
## Vector Fields
A vector field assigns a vector to every point in space. In 2D, such a field can be represented as
$$\vec{F}(x, y) = (F_x(x, y), F_y(x, y))$$
where $F_x$ and $F_y$ are the components of the vector field. Both may depend on the position $(x, y)$.
### Example: Static vector field
Let us examine a vector field that depends on both space and time:
$$
\vec{F}(x, y) = (\sin(y), \sqrt{\frac{|x|}{5}})
$$
This field might represent the velocity of a fluid at each point $(x, y)$ in a 2D space. Below is a Python code to visualize this vector field:
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Definition of the vector field
def vector_field(x, y):
Fx = np.sin(y)
Fy = np.sqrt(np.abs(x)/5)
return Fx, Fy
# Generating a grid of points
x = np.linspace(-15, 15, 16)
y = np.linspace(-15, 15, 16)
x, y = np.meshgrid(x, y)
# Calculating the vector field
Fx, Fy = vector_field(x, y)
# Calculating the lengths of the vectors
d_lengths = np.sqrt(Fx**2 + Fy**2)
# Initializing the plot
fig, ax = plt.subplots(figsize=(6, 6))
quiver = ax.quiver(
x, y, Fx, Fy, d_lengths, angles='xy', scale_units='xy', scale=1, cmap='viridis'
)
cb = fig.colorbar(quiver, ax=ax, label="Vector length")
ax.set_xlim(-15, 15)
ax.set_ylim(-15, 15)
ax.set_xlabel("X axis")
ax.set_ylabel("Y axis")
ax.set_title("2D vector field with color dependent on vector length")
plt.show()
```
Stream plot
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Definition of the vector field
def vector_field(x, y):
Fx = np.sin(y)
Fy = np.sqrt(np.abs(x)/5)
return Fx, Fy
# Generating a grid of points
x = np.linspace(-15, 15, 100) # Higher resolution for streamlines
y = np.linspace(-15, 15, 100)
x, y = np.meshgrid(x, y)
# Calculating the vector field
Fx, Fy = vector_field(x, y)
d_lengths = np.sqrt(Fx**2 + Fy**2)
# Streamplot (Streamlines)
fig, ax = plt.subplots(figsize=(6, 6))
stream = ax.streamplot(
x[0, :], y[:, 0], Fx, Fy, color=d_lengths, cmap='viridis', linewidth=1.5
)
cb = fig.colorbar(stream.lines, ax=ax, label="Vector length")
ax.set_xlim(-15, 15)
ax.set_ylim(-15, 15)
ax.set_xlabel("X axis")
ax.set_ylabel("Y axis")
ax.set_title("2D Vector Field with Streamlines")
plt.show()
```
### Examples in html
```{=html}
<div class="river-simulation">
<style>
/* Zmieniono selektor z 'body' na klasę kontenera, aby nie psuć reszty strony */
.river-simulation {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
background-color: #f4f4f4;
color: #333;
user-select: none; /* To teraz działa tylko wewnątrz symulacji */
}
.river-simulation .container {
background-color: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 800px;
width: 100%;
text-align: center;
}
.river-simulation canvas {
border-left: 0;
border-right: 0;
background-color: #e3f2fd;
cursor: crosshair;
margin: 0 auto;
width: 100%;
max-width: 600px;
height: 300px;
display: block;
touch-action: none; /* Important for touch dragging */
}
.river-simulation .river-bank {
height: 20px;
width: 100%;
max-width: 600px;
background-color: #5d4037;
margin: 0 auto;
position: relative;
z-index: 1;
}
.river-simulation .bank-top { border-radius: 4px 4px 0 0; border-bottom: 2px solid #3e2723; }
.river-simulation .bank-bottom { border-radius: 0 0 4px 4px; border-top: 2px solid #3e2723; }
.river-simulation .controls {
margin: 15px 0;
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.river-simulation button {
padding: 8px 16px;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: opacity 0.2s;
min-width: 100px;
}
.river-simulation button:hover { opacity: 0.9; }
.river-simulation .btn-pause { background-color: #f57c00; }
.river-simulation .btn-resume { background-color: #2e7d32; }
.river-simulation .btn-clear { background-color: #d32f2f; }
.river-simulation .slider-group {
display: flex;
align-items: center;
gap: 10px;
background: #f5f5f5;
padding: 5px 10px;
border-radius: 4px;
}
.river-simulation label {
font-weight: 600;
font-size: 0.9em;
}
.river-simulation input[type=range] {
cursor: pointer;
}
.river-simulation .status {
font-size: 0.85em;
color: #666;
margin-top: 5px;
font-style: italic;
}
</style>
<div class="container">
<div class="controls">
<button id="pauseBtn" class="btn-pause">Pause</button>
<button id="clearBtn" class="btn-clear">Clear Particles</button>
<div class="slider-group">
<label for="speedSlider">Flow Speed (V<sub>max</sub>): <span id="speedVal">5.0</span></label>
<input type="range" id="speedSlider" min="0" max="15" step="0.5" value="5">
</div>
</div>
<div class="river-bank bank-top"></div>
<canvas id="simulationCanvas" width="600" height="300"></canvas>
<div class="river-bank bank-bottom"></div>
<p class="status">Click and drag to place particles. Pause to arrange them, then Resume.</p>
</div>
<script>
(function() { // Funkcja IIFE, żeby zmienne nie wyciekały globalnie
const canvas = document.getElementById('simulationCanvas');
const ctx = canvas.getContext('2d');
const speedSlider = document.getElementById('speedSlider');
const speedVal = document.getElementById('speedVal');
const clearBtn = document.getElementById('clearBtn');
const pauseBtn = document.getElementById('pauseBtn');
// Simulation State
let particles = [];
let maxSpeed = parseFloat(speedSlider.value);
let isPaused = false;
let isDragging = false;
// Constants
const GRID_SPACING = 40;
const PARTICLE_COLOR = '#d32f2f';
const VECTOR_COLOR = 'rgba(0, 60, 160, 0.4)';
// --- Physics & Math ---
// Velocity profile: V(y) = Vmax * sin(y_normalized * PI)
function getVelocity(y) {
const H = canvas.height;
// Map y to 0..PI range
const arg = (y / H) * Math.PI;
// Prevent negative speeds (just in case)
if (arg < 0 || arg > Math.PI) return 0;
return maxSpeed * Math.sin(arg);
}
// --- Drawing ---
function drawArrow(x, y, vel) {
const visualScale = 6.0;
const length = vel * visualScale;
// Do not draw tiny arrows
if (length < 2) return;
ctx.strokeStyle = VECTOR_COLOR;
ctx.fillStyle = VECTOR_COLOR;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + length, y);
ctx.stroke();
// Arrowhead
const headSize = 5;
ctx.beginPath();
ctx.moveTo(x + length, y);
ctx.lineTo(x + length - headSize, y - headSize * 0.6);
ctx.lineTo(x + length - headSize, y + headSize * 0.6);
ctx.fill();
}
function drawVectorField() {
// Draw a grid of vectors
for (let y = GRID_SPACING / 2; y < canvas.height; y += GRID_SPACING) {
for (let x = GRID_SPACING / 2; x < canvas.width; x += GRID_SPACING) {
const v = getVelocity(y);
drawArrow(x, y, v);
}
}
}
function drawParticles() {
ctx.fillStyle = PARTICLE_COLOR;
for (let p of particles) {
ctx.beginPath();
ctx.arc(p.x, p.y, 5, 0, Math.PI * 2);
ctx.fill();
}
}
// --- Main Loop ---
function update() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 1. Draw Background (Vector Field)
drawVectorField();
// 2. Physics Update (only if running)
if (!isPaused) {
for (let p of particles) {
const v = getVelocity(p.y);
p.x += v;
}
// Remove particles that went off-screen
particles = particles.filter(p => p.x < canvas.width + 10);
}
// 3. Draw Particles
drawParticles();
requestAnimationFrame(update);
}
// --- Interaction ---
function addParticle(clientX, clientY) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (clientX - rect.left) * scaleX;
const y = (clientY - rect.top) * scaleY;
// Simple collision check to prevent stacking too many particles in one spot
const tooClose = particles.some(p => Math.abs(p.x - x) < 5 && Math.abs(p.y - y) < 5);
if (!tooClose) {
particles.push({ x, y });
}
}
// Mouse Events
canvas.addEventListener('mousedown', (e) => {
isDragging = true;
addParticle(e.clientX, e.clientY);
});
window.addEventListener('mousemove', (e) => {
if (isDragging) {
addParticle(e.clientX, e.clientY);
}
});
window.addEventListener('mouseup', () => {
isDragging = false;
});
// Touch Events (for mobile/tablet)
canvas.addEventListener('touchstart', (e) => {
e.preventDefault(); // Prevent scrolling
isDragging = true;
addParticle(e.touches[0].clientX, e.touches[0].clientY);
}, { passive: false });
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (isDragging) {
addParticle(e.touches[0].clientX, e.touches[0].clientY);
}
}, { passive: false });
window.addEventListener('touchend', () => {
isDragging = false;
});
// UI Controls
pauseBtn.addEventListener('click', () => {
isPaused = !isPaused;
if (isPaused) {
pauseBtn.innerText = "Resume";
pauseBtn.className = "btn-resume";
} else {
pauseBtn.innerText = "Pause";
pauseBtn.className = "btn-pause";
}
});
clearBtn.addEventListener('click', () => {
particles = [];
});
speedSlider.addEventListener('input', (e) => {
maxSpeed = parseFloat(e.target.value);
speedVal.innerText = maxSpeed.toFixed(1);
});
// Start
update();
})();
</script>
</div>
```
## Derivatives and Integrals
### Derivatives
#### Derivative of functions of one variable
The derivative of a function $f(x)$ with respect to $x$ is defined as
$$
\frac{df(x)}{dx} = \lim_{h \to 0} \frac{f(x + h) - f(x)}{h}
$$
This limit represents the rate of change of the function $f(x)$ with respect to $x$.
#### Derivative of functions of two variables
The derivative of a function $f(x, y)$ with respect to $x$ is defined as
$$
\frac{\partial f(x, y)}{\partial x} = \lim_{h \to 0} \frac{f(x + h, y) - f(x, y)}{h}
$$
This limit represents the rate of change of the function $f(x, y)$ with respect to $x$. The derivative is a measure of how the function changes as $x$ changes for a fixed value of $y$. This describes how the function changes in the $x$ direction.
Similarly, the derivative of a function $f(x, y)$ with respect to $y$ is defined as
$$
\frac{\partial f(x, y)}{\partial y} = \lim_{h \to 0} \frac{f(x, y + h) - f(x, y)}{h}
$$
This limit represents the rate of change of the function $f(x, y)$ with respect to $y$ and describes how the function changes in the $y$ direction for a fixed value of $x$.
#### Html example
```{=html}
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wizualizacja Pochodnych</title>
<!-- Import Plotly -->
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
<style>
body {
margin: 0;
padding: 0;
background-color: #f4f4f4;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
}
.derivative-widget {
width: 100%;
max-width: 1000px;
margin: 20px;
padding: 10px;
background-color: #f4f4f4;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
border-radius: 8px;
}
.derivative-widget .vis-container {
width: 100%;
background-color: white;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
border-radius: 8px;
display: flex;
flex-direction: column;
height: 650px;
}
/* Tabs styling */
.derivative-widget .tabs {
display: flex;
background-color: #e0e0e0;
border-radius: 8px 8px 0 0;
}
.derivative-widget .tab-btn {
flex: 1;
padding: 15px;
border: none;
background-color: transparent;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: background-color 0.3s;
border-bottom: 3px solid transparent;
}
.derivative-widget .tab-btn:hover {
background-color: #d0d0d0;
}
.derivative-widget .tab-btn.active {
background-color: white;
border-bottom: 3px solid #1976d2;
color: #1976d2;
}
/* Content styling */
.derivative-widget .tab-content {
display: none;
flex: 1;
padding: 20px;
flex-direction: column;
overflow: hidden;
}
.derivative-widget .tab-content.active {
display: flex;
}
.derivative-widget .controls {
display: flex;
gap: 20px;
margin-bottom: 15px;
align-items: center;
flex-wrap: wrap;
padding: 15px;
background-color: #f9f9f9;
border-radius: 8px;
border: 1px solid #eee;
justify-content: space-between; /* Rozsunięcie elementów */
}
.derivative-widget .control-group {
display: flex;
flex-direction: column;
min-width: 150px;
}
/* Styl dla boksu z wynikami */
.derivative-widget .result-box {
background-color: #fff;
padding: 10px 15px;
border-radius: 6px;
border-left: 4px solid #1976d2;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
min-width: 220px;
}
.derivative-widget label {
font-size: 0.9em;
font-weight: 600;
margin-bottom: 5px;
color: #555;
}
.derivative-widget input[type=range] {
cursor: pointer;
}
.derivative-widget .plot-area {
flex: 1;
width: 100%;
min-height: 400px;
position: relative;
}
.derivative-widget .math-val {
font-family: monospace;
color: #d32f2f;
font-weight: bold;
}
.derivative-widget .math-result {
font-family: monospace;
color: #1976d2;
font-weight: bold;
font-size: 1.2em;
}
</style>
</head>
<body>
<div class="derivative-widget">
<div class="vis-container">
<div class="tabs">
<button class="tab-btn active" data-tab="ordinary">Ordinary Derivative (1D)</button>
<button class="tab-btn" data-tab="partial">Partial Derivatives (2D/3D)</button>
</div>
<div id="tab-ordinary" class="tab-content active">
<div class="controls">
<div style="display:flex; gap: 20px; flex-wrap: wrap;">
<div class="control-group">
<label>Point position (x): <span id="ord-x-val" class="math-val">1.00</span></label>
<input type="range" id="ord-x" min="-2.5" max="2.5" step="0.05" value="1.0">
</div>
<div class="control-group">
<label>Step size (h): <span id="ord-h-val" class="math-val">1.00</span></label>
<input type="range" id="ord-h" min="0.01" max="2.0" step="0.01" value="1.0">
</div>
</div>
<!-- Boks z obliczeniami -->
<div class="control-group result-box">
<div title="Iloraz różnicowy (nachylenie siecznej)">
Δy / Δx ≈ <span id="calc-slope" class="math-result"></span>
</div>
<div style="font-size: 0.85em; color: #666; margin-top: 4px;">
Exact derivative f'(x): <span id="calc-exact" style="font-weight:600"></span>
</div>
</div>
<div style="width: 100%; font-size: 0.85em; color: #666; margin-top: 10px;">
Function: <span style="font-family: monospace">f(x) = 0.5x³ - x</span><br>
Move 'h' close to 0 to see the approximation converge to the exact value.
</div>
</div>
<div id="plot-ordinary" class="plot-area"></div>
</div>
<div id="tab-partial" class="tab-content">
<div class="controls">
<div class="control-group">
<label>X position: <span id="part-x-val" class="math-val">1.00</span></label>
<input type="range" id="part-x" min="-2.5" max="2.5" step="0.1" value="1.0">
</div>
<div class="control-group">
<label>Y position: <span id="part-y-val" class="math-val">1.00</span></label>
<input type="range" id="part-y" min="-2.5" max="2.5" step="0.1" value="1.0">
</div>
<div style="font-size: 0.85em; color: #666;">
Function: <span style="font-family: monospace">f(x,y) = sin(x) * cos(y)</span><br>
<span style="color:red">Red:</span> Slice y=const (∂f/∂x).
<span style="color:blue">Blue:</span> Slice x=const (∂f/∂y).
</div>
</div>
<div id="plot-partial" class="plot-area"></div>
</div>
</div>
</div>
<script>
(function() {
const widget = document.querySelector('.derivative-widget');
function switchTab(tabName) {
widget.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
widget.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
widget.querySelector('#tab-' + tabName).classList.add('active');
widget.querySelector(`.tab-btn[data-tab="${tabName}"]`).classList.add('active');
window.dispatchEvent(new Event('resize'));
}
widget.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', function() {
switchTab(this.getAttribute('data-tab'));
});
});
// --- TAB 1 LOGIC: ORDINARY DERIVATIVE ---
const ordXSlider = document.getElementById('ord-x');
const ordHSlider = document.getElementById('ord-h');
const ordXVal = document.getElementById('ord-x-val');
const ordHVal = document.getElementById('ord-h-val');
// Elementy do wyświetlania obliczeń
const calcSlope = document.getElementById('calc-slope');
const calcExact = document.getElementById('calc-exact');
function f1(x) { return 0.5 * Math.pow(x, 3) - x; }
function df1(x) { return 1.5 * Math.pow(x, 2) - 1; }
function drawOrdinary() {
const x0 = parseFloat(ordXSlider.value);
const h = parseFloat(ordHSlider.value);
const x1 = x0 + h;
ordXVal.innerText = x0.toFixed(2);
ordHVal.innerText = h.toFixed(2);
const xRange = [], yRange = [];
for (let i = -3.5; i <= 3.5; i += 0.1) {
xRange.push(i);
yRange.push(f1(i));
}
const y0 = f1(x0);
const y1 = f1(x1);
// Secant calculation (Difference Quotient)
const m_secant = (y1 - y0) / (x1 - x0);
// Tangent calculation (Exact Derivative)
const m_tangent = df1(x0);
// Aktualizacja wyświetlacza obliczeń
calcSlope.innerText = m_secant.toFixed(4);
calcExact.innerText = m_tangent.toFixed(4);
const secantX = [x0 - 2, x1 + 2];
const secantY = [m_secant * (secantX[0] - x0) + y0, m_secant * (secantX[1] - x0) + y0];
const tangentX = [x0 - 1.5, x0 + 1.5];
const tangentY = [m_tangent * (tangentX[0] - x0) + y0, m_tangent * (tangentX[1] - x0) + y0];
const triX = [x0, x1, x1, x0];
const triY = [y0, y0, y1, y0];
const data = [
{ x: xRange, y: yRange, mode: 'lines', line: {color: '#444', width: 2}, name: 'f(x)' },
{ x: triX, y: triY, mode: 'lines', line: {color: 'rgba(0,0,0,0.3)', width: 1, dash: 'dot'}, fill: 'toself', fillColor: 'rgba(0,255,0,0.1)', name: 'Δx, Δy', showlegend: false },
{ x: secantX, y: secantY, mode: 'lines', line: {color: '#2e7d32', width: 3}, name: 'Secant (Approx)' },
{ x: tangentX, y: tangentY, mode: 'lines', line: {color: '#d32f2f', width: 2, dash: 'dash'}, name: 'Tangent (Exact)' },
{ x: [x0], y: [y0], mode: 'markers', marker: {color: '#d32f2f', size: 10}, name: 'x' },
{ x: [x1], y: [y1], mode: 'markers', marker: {color: '#2e7d32', size: 10}, name: 'x + h' }
];
const layout = {
title: 'Difference Quotient vs Derivative',
xaxis: {title: 'x', range: [-3, 3]},
yaxis: {title: 'f(x)', range: [-3, 3]},
margin: {l: 50, r: 20, t: 40, b: 40},
showlegend: true,
legend: {x: 0, y: 1},
autosize: true
};
// Używamy react dla płynności
Plotly.react('plot-ordinary', data, layout, {responsive: true, displayModeBar: false});
}
ordXSlider.addEventListener('input', drawOrdinary);
ordHSlider.addEventListener('input', drawOrdinary);
// --- TAB 2 LOGIC: PARTIAL DERIVATIVES ---
const partXSlider = document.getElementById('part-x');
const partYSlider = document.getElementById('part-y');
const partXVal = document.getElementById('part-x-val');
const partYVal = document.getElementById('part-y-val');
function f2(x, y) { return Math.sin(x) * Math.cos(y); }
function df2_dx(x, y) { return Math.cos(x) * Math.cos(y); }
function df2_dy(x, y) { return -Math.sin(x) * Math.sin(y); }
function drawPartial() {
const x0 = parseFloat(partXSlider.value);
const y0 = parseFloat(partYSlider.value);
const z0 = f2(x0, y0);
partXVal.innerText = x0.toFixed(2);
partYVal.innerText = y0.toFixed(2);
const range = 3.5;
const step = 0.2;
const z_data = [], x_axis = [], y_axis = [];
for (let y = -range; y <= range; y += step) {
const row = [];
y_axis.push(y);
for (let x = -range; x <= range; x += step) {
if(y === -range) x_axis.push(x);
row.push(f2(x, y));
}
z_data.push(row);
}
const sliceX_x=[], sliceX_y=[], sliceX_z=[];
for(let i=-range; i<=range; i+=0.1) { sliceX_x.push(i); sliceX_y.push(y0); sliceX_z.push(f2(i, y0)); }
const sliceY_x=[], sliceY_y=[], sliceY_z=[];
for(let i=-range; i<=range; i+=0.1) { sliceY_x.push(x0); sliceY_y.push(i); sliceY_z.push(f2(x0, i)); }
const slopeX = df2_dx(x0, y0);
const tanX_x = [x0 - 1, x0 + 1], tanX_y = [y0, y0], tanX_z = [z0 - slopeX, z0 + slopeX];
const slopeY = df2_dy(x0, y0);
const tanY_x = [x0, x0], tanY_y = [y0 - 1, y0 + 1], tanY_z = [z0 - slopeY, z0 + slopeY];
const data = [
{ z: z_data, x: x_axis, y: y_axis, type: 'surface', colorscale: 'Viridis', opacity: 0.6, showscale: false, name: 'Surface' },
{ type: 'scatter3d', mode: 'lines', x: sliceX_x, y: sliceX_y, z: sliceX_z, line: {width: 5, color: 'red'}, name: 'Cut y=const' },
{ type: 'scatter3d', mode: 'lines', x: tanX_x, y: tanX_y, z: tanX_z, line: {width: 8, color: '#ff8a80'}, name: 'Tangent X' },
{ type: 'scatter3d', mode: 'lines', x: sliceY_x, y: sliceY_y, z: sliceY_z, line: {width: 5, color: 'blue'}, name: 'Cut x=const' },
{ type: 'scatter3d', mode: 'lines', x: tanY_x, y: tanY_y, z: tanY_z, line: {width: 8, color: '#82b1ff'}, name: 'Tangent Y' },
{ type: 'scatter3d', mode: 'markers', x: [x0], y: [y0], z: [z0], marker: {size: 6, color: 'black'}, name: 'Point P' }
];
const layout = {
title: 'Partial Derivatives (Slices)',
margin: {l: 0, r: 0, b: 0, t: 30},
scene: { aspectmode: 'cube', xaxis: {title: 'X'}, yaxis: {title: 'Y'}, zaxis: {title: 'Z'} },
showlegend: true,
legend: {x: 0, y: 1},
autosize: true
};
Plotly.react('plot-partial', data, layout, {responsive: true, displayModeBar: false});
}
partXSlider.addEventListener('input', drawPartial);
partYSlider.addEventListener('input', drawPartial);
drawOrdinary();
setTimeout(drawPartial, 100);
window.addEventListener('resize', () => {
Plotly.Plots.resize('plot-ordinary');
Plotly.Plots.resize('plot-partial');
});
})();
</script>
</body>
</html>
```
### Integrals
#### Area under a curve
At the simplest level, an integral is the reverse of a derivative. The integral of a function $f(x)$ with respect to $x$ is defined as
$$
\int f(x) dx = F(x) + C
$$
where $F(x)$ is the antiderivative of $f(x)$ and $C$ is an integration constant. The antiderivative is a function whose derivative is equal to $f(x)$:
$$
\frac{dF(x)}{dx} = f(x)
$$
The integral is a measure of the area under the curve of the function $f(x)$. The integral is a function that gives the area under the curve of the function $f(x)$ up to a given point.
#### Line integrals
The line integral is a generalization of the integral to functions of multiple variables. The line integral of a vector field $\mathbf{F}(x, y)$ along a curve $C$ parameterized by $\mathbf{r}(t) = (x(t), y(t))$ for $t$ in $[a, b]$ is defined as
$$
\int_C \mathbf{F} \cdot d\mathbf{r} = \int_a^b \mathbf{F}(\mathbf{r}(t)) \cdot \mathbf{r}'(t) dt
$$
where $\mathbf{F} \cdot d\mathbf{r}$ is the dot product of the vector field $\mathbf{F}$ and the differential element of the curve $d\mathbf{r}$. The line integral is a measure of the work done by the vector field $\mathbf{F}$ along the curve $C$.
Numerically, the line integral can be approximated by dividing the curve into $N$ small segments and summing the contributions from each segment:
$$
\int_C \mathbf{F} \cdot d\mathbf{r} \approx \sum_{i=1}^{N} \mathbf{F}(\mathbf{r}(t_i)) \cdot \Delta \mathbf{r}_i
$$
where $\Delta \mathbf{r}_i$ is the vector representing the $i$-th segment of the curve.
### Html example
```{=html}
<!-- Load Plotly once per page. If your Quarto doc already loads it, you can remove this script tag. -->
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
<div class="integration-vis-container" style="font-family: sans-serif; border: 1px solid #ddd; border-radius: 8px; background: #fff; margin: 20px 0; overflow: hidden; max-width: 100%;">
<!-- TABS HEADER -->
<div style="display: flex; background: #f5f5f5; border-bottom: 1px solid #ddd;">
<button id="btn-tab-riemann" style="flex: 1; padding: 15px; border: none; background: transparent; cursor: pointer; font-weight: bold; color: #555; border-bottom: 3px solid transparent;">
1. Definite Integral (Area)
</button>
<button id="btn-tab-line" style="flex: 1; padding: 15px; border: none; background: transparent; cursor: pointer; font-weight: bold; color: #555; border-bottom: 3px solid transparent;">
2. Line Integral (Work)
</button>
</div>
<!-- TAB 1: RIEMANN SUMS -->
<div id="content-riemann" style="display: block; padding: 15px;">
<div style="background: #fafafa; padding: 10px; border: 1px solid #eee; margin-bottom: 10px; border-radius: 4px;">
<label style="font-weight: bold;">Rectangles (N): <span id="disp-rie-n">5</span></label>
<input type="range" id="input-rie-n" min="2" max="50" step="1" value="5" style="vertical-align: middle; margin-left: 10px;">
<div style="margin-top: 5px; font-family: monospace; color: #333;">
Area ≈ <span id="disp-rie-sum" style="color: #d32f2f; font-weight: bold;">0.00</span>
<span style="color: #777; font-size: 0.9em; margin-left: 10px;">( f(x) = 1 + 0.1x² + 0.5sin(2x) )</span>
</div>
</div>
<!-- IMPORTANT: Explicit height prevents collapse -->
<div id="plot-riemann-div" style="width: 100%; height: 400px;"></div>
</div>
<!-- TAB 2: LINE INTEGRAL -->
<div id="content-line" style="display: none; padding: 15px;">
<div style="background: #fafafa; padding: 10px; border: 1px solid #eee; margin-bottom: 10px; border-radius: 4px; display: flex; align-items: center; flex-wrap: wrap; gap: 15px;">
<div>
<label style="font-weight: bold;">Segments (N): <span id="disp-line-n">10</span></label>
<input type="range" id="input-line-n" min="2" max="40" step="1" value="10" style="vertical-align: middle;">
</div>
<button id="btn-clear-path" style="background: #d32f2f; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; font-weight: bold;">Clear Path</button>
<div style="font-family: monospace; color: #333;">
Work ≈ <span id="disp-line-work" style="color: #d32f2f; font-weight: bold;">0.00</span>
</div>
</div>
<!-- Canvas Container with explicit relative positioning -->
<div id="canvas-wrapper" style="width: 100%; height: 400px; position: relative; border: 1px solid #eee; background: #fff;">
<canvas id="canvas-line" style="display: block; width: 100%; height: 100%; cursor: crosshair; touch-action: none;"></canvas>
<div style="position: absolute; top: 10px; left: 10px; background: rgba(255,255,255,0.9); padding: 5px 8px; pointer-events: none; border: 1px solid #ccc; font-size: 0.85em; border-radius: 4px;">
Draw with mouse/touch<br>
<span style="color: #d32f2f; font-weight: bold;">Red: Force</span> | <span style="color: #2e7d32; font-weight: bold;">Green: Displacement</span>
</div>
</div>
</div>
</div>
<script>
// Use IIFE to isolate variables from the rest of the page
(function() {
// --- ELEMENTS ---
const btnTabRie = document.getElementById('btn-tab-riemann');
const btnTabLine = document.getElementById('btn-tab-line');
const contentRie = document.getElementById('content-riemann');
const contentLine = document.getElementById('content-line');
const inputRieN = document.getElementById('input-rie-n');
const dispRieN = document.getElementById('disp-rie-n');
const dispRieSum = document.getElementById('disp-rie-sum');
const plotRieDiv = document.getElementById('plot-riemann-div');
const inputLineN = document.getElementById('input-line-n');
const dispLineN = document.getElementById('disp-line-n');
const dispLineWork = document.getElementById('disp-line-work');
const btnClearPath = document.getElementById('btn-clear-path');
const canvas = document.getElementById('canvas-line');
const canvasWrapper = document.getElementById('canvas-wrapper');
const ctx = canvas.getContext('2d');
// --- TAB LOGIC ---
function setActiveTab(tab) {
if (tab === 'riemann') {
contentRie.style.display = 'block';
contentLine.style.display = 'none';
btnTabRie.style.borderBottomColor = '#1976d2';
btnTabRie.style.color = '#1976d2';
btnTabLine.style.borderBottomColor = 'transparent';
btnTabLine.style.color = '#555';
// Trigger Plotly resize after tab switch
if (window.Plotly) Plotly.Plots.resize(plotRieDiv);
} else {
contentRie.style.display = 'none';
contentLine.style.display = 'block';
btnTabRie.style.borderBottomColor = 'transparent';
btnTabRie.style.color = '#555';
btnTabLine.style.borderBottomColor = '#1976d2';
btnTabLine.style.color = '#1976d2';
resizeCanvas();
}
}
btnTabRie.onclick = () => setActiveTab('riemann');
btnTabLine.onclick = () => setActiveTab('line');
// ==========================================
// 1. RIEMANN SUMS
// ==========================================
function f(x) {
return 1 + 0.1 * x * x + 0.5 * Math.sin(2 * x);
}
function updateRiemann() {
const N = parseInt(inputRieN.value);
dispRieN.innerText = N;
const a = 0, b = 6;
const dx = (b - a) / N;
// Curve
const xSmooth = [], ySmooth = [];
for(let x=a; x<=b; x+=0.05) {
xSmooth.push(x);
ySmooth.push(f(x));
}
// Rectangles
const xRect = [], yRect = [];
let area = 0;
for(let i=0; i<N; i++) {
let xi = a + i*dx;
let val = f(xi);
xRect.push(xi + dx/2);
yRect.push(val);
area += val * dx;
}
dispRieSum.innerText = area.toFixed(4);
const data = [
{
x: xRect, y: yRect, width: dx*0.9, type: 'bar',
marker: { color: 'rgba(50, 171, 96, 0.6)', line: {color: 'rgba(50, 171, 96, 1)', width: 1} },
name: 'Riemann Sum'
},
{
x: xSmooth, y: ySmooth, mode: 'lines',
line: { color: '#1976d2', width: 3 },
name: 'f(x)'
}
];
const layout = {
margin: { t: 10, l: 40, r: 10, b: 30 },
xaxis: { title: 'x' },
yaxis: { title: 'f(x)', range: [0, 5] },
showlegend: false,
autosize: true
};
Plotly.newPlot(plotRieDiv, data, layout, {responsive: true, displayModeBar: false});
}
inputRieN.addEventListener('input', updateRiemann);
// ==========================================
// 2. LINE INTEGRAL (CANVAS)
// ==========================================
let pathPoints = [];
let isDrawing = false;
function resizeCanvas() {
const rect = canvasWrapper.getBoundingClientRect();
if(rect.width > 0 && rect.height > 0) {
canvas.width = rect.width;
canvas.height = rect.height;
drawScene();
}
}
function getField(cx, cy, w, h) {
// Simple rotation field relative to center
const mx = (cx - w/2) / (w/4);
const my = -(cy - h/2) / (h/4); // Y inverted for math
return { fx: -my, fy: mx * 0.5 };
}
function drawArrow(x, y, dx, dy, color) {
const head = 6;
const angle = Math.atan2(dy, dx);
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.moveTo(x, y);
ctx.lineTo(x + dx, y + dy);
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = color;
ctx.moveTo(x + dx, y + dy);
ctx.lineTo(x + dx - head*Math.cos(angle-Math.PI/6), y + dy - head*Math.sin(angle-Math.PI/6));
ctx.lineTo(x + dx - head*Math.cos(angle+Math.PI/6), y + dy - head*Math.sin(angle+Math.PI/6));
ctx.fill();
}
function drawScene() {
const w = canvas.width;
const h = canvas.height;
ctx.clearRect(0,0,w,h);
// Grid (Field)
const step = 40;
for(let y=20; y<h; y+=step) {
for(let x=20; x<w; x+=step) {
const f = getField(x,y,w,h);
drawArrow(x, y, f.fx*15, -f.fy*15, '#e0e0e0');
}
}
// Path
if(pathPoints.length > 1) {
ctx.beginPath();
ctx.strokeStyle = '#1976d2';
ctx.lineWidth = 2;
ctx.moveTo(pathPoints[0].x, pathPoints[0].y);
for(let i=1; i<pathPoints.length; i++) ctx.lineTo(pathPoints[i].x, pathPoints[i].y);
ctx.stroke();
}
// Vectors & Work
if(pathPoints.length > 1) {
const N = parseInt(inputLineN.value);
dispLineN.innerText = N;
// Calc Lengths
let dists = [0], totalLen = 0;
for(let i=1; i<pathPoints.length; i++) {
let dx = pathPoints[i].x - pathPoints[i-1].x;
let dy = pathPoints[i].y - pathPoints[i-1].y;
totalLen += Math.sqrt(dx*dx + dy*dy);
dists.push(totalLen);
}
// Resample
let work = 0;
let samplePts = [];
for(let k=0; k<=N; k++) {
let target = (k/N) * totalLen;
// Find index
let idx = 0;
while(idx < dists.length-1 && dists[idx+1] < target) idx++;
let t = (target - dists[idx]) / (dists[idx+1] - dists[idx] || 1);
let p1 = pathPoints[idx];
let p2 = pathPoints[idx+1] || p1;
samplePts.push({
x: p1.x + (p2.x - p1.x)*t,
y: p1.y + (p2.y - p1.y)*t
});
}
// Draw
for(let i=0; i<N; i++) {
let p1 = samplePts[i];
let p2 = samplePts[i+1];
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
let mx = (p1.x+p2.x)/2;
let my = (p1.y+p2.y)/2;
let f = getField(mx, my, w, h);
// Work calc (normalized)
work += (f.fx * (dx/100) + f.fy * (-dy/100));
drawArrow(p1.x, p1.y, dx, dy, '#2e7d32'); // Disp
drawArrow(p1.x, p1.y, f.fx*25, -f.fy*25, '#d32f2f'); // Force
ctx.fillStyle='black'; ctx.beginPath(); ctx.arc(p1.x, p1.y, 3, 0, 6.28); ctx.fill();
}
dispLineWork.innerText = (work*5).toFixed(2);
}
}
// Interaction
function getXY(e) {
const r = canvas.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
return { x: clientX - r.left, y: clientY - r.top };
}
canvas.addEventListener('mousedown', e => { isDrawing=true; pathPoints=[getXY(e)]; drawScene(); });
window.addEventListener('mousemove', e => {
if(!isDrawing) return;
pathPoints.push(getXY(e));
drawScene();
});
window.addEventListener('mouseup', () => { isDrawing=false; drawScene(); });
// Touch
canvas.addEventListener('touchstart', e => {
e.preventDefault(); // Stop scroll inside canvas ONLY
isDrawing=true;
pathPoints=[getXY(e)];
drawScene();
}, {passive:false});
window.addEventListener('touchmove', e => {
if(isDrawing) {
e.preventDefault();
pathPoints.push(getXY(e));
drawScene();
}
}, {passive:false});
window.addEventListener('touchend', () => isDrawing=false);
btnClearPath.onclick = () => { pathPoints=[]; dispLineWork.innerText="0.00"; drawScene(); };
inputLineN.oninput = drawScene;
// INIT
setActiveTab('riemann');
updateRiemann();
// Safety timeout for Plotly to render after DOM placement
setTimeout(() => {
updateRiemann();
resizeCanvas();
}, 500);
})();
</script>
```
## Differential equations
### Starting example
Let's consider the first order differential equation
$$
\frac{dy(x)}{dx} = 2x
$$
Let us think how we should read this equation. The left hand side is the derivative of the function $y(x)$ with respect to $x$. This derivative is equal to $2x$. This equation tells us how the function $y(x)$ changes with $x$. The function $y(x)$ changes at a rate of $2x$ with respect to $x$. This is a simple differential equation that can be solved by integration.
$$
y(x) = x^2 + C
$$
where $C$ is an integration constant. This is the general solution of the differential equation. The solution is not unique because the constant $C$ can take any value.
```{python}
import numpy as np
import matplotlib.pyplot as plt
def y(x, C):
return x**2 + C
x = np.linspace(0, 10, 100)
fig, ax = plt.subplots(figsize=(6, 4))
for C in range(-50, 50, 10):
plt.plot(x, y(x, C), label=f'C={C}')
plt.xlabel('x')
plt.ylabel('y')
plt.show()
```
##### Numerical solution
Remember that derivatives can be approximated by finite differences. The derivative of a function $f(x)$ can be approximated by
$$
\frac{df(x)}{dx} \approx \frac{f(x + h) - f(x)}{h}
$$
where $h$ is a small number. This is a simple way to approximate the derivative of a function. Let's use this approximation to solve the differential equation
$$
\frac{dy(x)}{dx} = 2x
$$
We can approximate the derivative by
$$
\frac{y(x + h) - y(x)}{h} = 2x
$$
This equation can be solved for $y(x + h)$
$$
y(x + h) = y(x) + 2xh
$$
This equation can be used to solve the differential equation numerically. We can start from an initial value of $y(x)$ and use the equation above to calculate the value of $y(x + h)$. This value can be used to calculate the next value of $y(x + 2h)$ and so on:
* Step 1 - we know value of $y$ for a given $x$ which is
$$y(x)$$
* Step 2 - we can calculate the value of $y$ for the next point $x + h$ using
$$y(x + h) = y(x) + 2xh$$
* Step 3 - we can calculate the value of $y$ for the next point $x + 2h$ using
$$y(x + 2h) = y(x + h) + 2(x + h)h$$
* Step 4 - we can calculate the value of $y$ for the next point $x + 3h$ using
$$y(x + 3h) = y(x + 2h) + 2(x + 2h)h$$
* and so on
Let us compare the numerical solution with the analytical solution.
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Define the derivative dy/dx
def dy_dx(x):
return 2 * x
# Define the exact analytical solution y(x) for comparison
def y_analytical(x):
return x**2
# Define a numerical solution for y(x) using the forward Euler method
def y_numerical(x, h):
y = [0] # Initialize y with the starting value, assuming y(0) = 0
for i in range(len(x) - 1):
y.append(y[-1] + dy_dx(x[i]) * h) # Update y using dy/dx
return y
# Set up the x values
plot_x = np.linspace(0, 10, 100)
x = np.linspace(0, 10, 10)
h = x[1] - x[0] # Step size
# Compute the difference between analytical and numerical solutions
difference = y_analytical(x) - np.array(y_numerical(x, h))
# Set up the figure with two subplots
fig, axs = plt.subplots(2,1, figsize=(10,8))
# Left plot: Analytical and numerical solutions
axs[0].plot(plot_x, y_analytical(plot_x), label='Analytical', linestyle='dashed', linewidth=2)
axs[0].scatter(x, y_numerical(x, h), label='Numerical', linestyle='solid', color='red')
axs[0].set_title('Analytical vs Numerical')
axs[0].set_xlabel('x')
axs[0].set_ylabel('y')
axs[0].legend()
# Right plot: Difference between solutions
axs[1].scatter(x, difference, label='Difference', color='red')
axs[1].set_title('Difference (Analytical - Numerical)')
axs[1].set_xlabel('x')
axs[1].set_ylabel('Difference')
axs[1].legend()
# Adjust layout and show the plots
plt.tight_layout()
plt.show()
```
Difference between the analytical and numerical solutions depends on the step size $h$.
#### Html example
```{=html}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Euler Method Visualization</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #fff;
display: flex;
justify-content: center;
}
/* Namespace .euler-method-sim to isolate styles */
.euler-method-sim {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0 auto;
padding: 10px;
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 8px;
color: #333;
width: 100%;
max-width: 800px;
box-sizing: border-box;
}
.euler-method-sim .sim-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
/* Wrapper must be relative for absolute positioning of the math panel */
.euler-method-sim .canvas-wrapper {
position: relative;
width: 100%;
max-width: 650px;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
.euler-method-sim canvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 16 / 10;
cursor: default;
}
/* --- Math Panel Styles --- */
.euler-method-sim .math-panel {
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #1976d2;
border-left: 5px solid #1976d2;
padding: 10px 15px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 13px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
pointer-events: none;
z-index: 10;
display: none;
line-height: 1.6;
}
.euler-method-sim .math-label {
color: #666;
font-weight: bold;
font-size: 11px;
text-transform: uppercase;
margin-bottom: 2px;
display: block;
border-bottom: 1px solid #eee;
}
.euler-method-sim .math-row {
white-space: nowrap;
}
.euler-method-sim .val-old { color: #d32f2f; font-weight: bold; }
.euler-method-sim .val-slope { color: #f57f17; font-weight: bold; }
.euler-method-sim .val-h { color: #333; font-weight: bold; }
.euler-method-sim .val-new { color: #1976d2; font-weight: bold; }
/* Controls */
.euler-method-sim .controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
width: 100%;
padding: 15px;
background: #eee;
border-radius: 6px;
}
.euler-method-sim .control-group {
display: flex;
flex-direction: column;
align-items: center;
min-width: 120px;
}
.euler-method-sim label {
font-size: 0.9em;
font-weight: 600;
margin-bottom: 5px;
}
.euler-method-sim .val-display {
font-family: monospace;
color: #d32f2f;
font-weight: bold;
}
.euler-method-sim .btn-group {
display: flex;
gap: 10px;
align-items: center;
}
.euler-method-sim button {
padding: 8px 16px;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
font-size: 14px;
transition: background 0.2s;
height: 40px;
}
.euler-method-sim #btn-restart { background-color: #1976d2; }
.euler-method-sim #btn-restart:hover { background-color: #1565c0; }
.euler-method-sim #btn-restart:disabled { background-color: #90caf9; cursor: not-allowed; }
.euler-method-sim #btn-clear { background-color: #757575; }
.euler-method-sim #btn-clear:hover { background-color: #616161; }
.euler-method-sim input[type=range] {
cursor: pointer;
}
.euler-method-sim .legend {
font-size: 0.85em;
display: flex;
gap: 15px;
margin-top: 5px;
flex-wrap: wrap;
justify-content: center;
}
.euler-method-sim .legend-item {
display: flex;
align-items: center;
gap: 5px;
}
.euler-method-sim .dot {
width: 10px; height: 10px; border-radius: 50%;
}
.euler-method-sim .line-sample {
width: 20px; height: 3px; border-radius: 2px;
}
</style>
</head>
<body>
<div class="euler-method-sim">
<div class="sim-container">
<div class="controls">
<div class="control-group">
<label>Initial y(0): <span id="disp-y0" class="val-display">0.0</span></label>
<input type="range" id="input-y0" min="-5" max="5" step="0.5" value="0">
</div>
<div class="control-group">
<label>Step size (h): <span id="disp-h" class="val-display">1.0</span></label>
<input type="range" id="input-h" min="0.1" max="2.0" step="0.1" value="1.0">
</div>
<div class="control-group">
<label>Animation Speed</label>
<input type="range" id="input-speed" min="1" max="10" step="1" value="5">
</div>
<div class="btn-group">
<button id="btn-restart">Start</button>
<button id="btn-clear">Clear History</button>
</div>
</div>
<div class="canvas-wrapper">
<canvas id="eulerCanvas" width="800" height="500"></canvas>
<!-- LIVE MATH PANEL -->
<div id="mathPanel" class="math-panel">
<span class="math-label">Step Calculation:</span>
<!-- y_new = y_old + y' * h -->
<!-- Zmieniono ċ na · aby uniknąć błędnego renderowania jako 'C z kropką' -->
<div class="math-row">y<sub>new</sub> = y<sub>old</sub> + y' · h</div>
<div class="math-row" id="mathCalc">
<!-- JS will insert values here -->
</div>
</div>
</div>
<div class="legend">
<div class="legend-item"><div class="line-sample" style="background:#1976d2"></div> Exact Solution</div>
<div class="legend-item"><div class="dot" style="background:#d32f2f"></div><div class="line-sample" style="background:#d32f2f"></div> Numerical (Current)</div>
<div class="legend-item"><div class="line-sample" style="background:#9e9e9e"></div> History</div>
<div class="legend-item"><div class="line-sample" style="background:#f57f17; height: 4px;"></div> Tangent (y')</div>
</div>
</div>
<script>
(function() {
const wrapper = document.querySelector('.euler-method-sim');
const canvas = wrapper.querySelector('#eulerCanvas');
const ctx = canvas.getContext('2d');
const mathPanel = wrapper.querySelector('#mathPanel');
const mathCalc = wrapper.querySelector('#mathCalc');
const inputY0 = wrapper.querySelector('#input-y0');
const inputH = wrapper.querySelector('#input-h');
const inputSpeed = wrapper.querySelector('#input-speed');
const btnRestart = wrapper.querySelector('#btn-restart');
const btnClear = wrapper.querySelector('#btn-clear');
const dispY0 = wrapper.querySelector('#disp-y0');
const dispH = wrapper.querySelector('#disp-h');
const LOGIC_X_MIN = -0.5;
const LOGIC_X_MAX = 6.0;
const LOGIC_Y_MIN = -10;
const LOGIC_Y_MAX = 40;
let state = {
y0: 0,
h: 1.0,
animationSpeed: 500,
currentStep: 0,
isAnimating: false,
points: [],
history: [],
timer: null
};
// Differential equation: y' = 2x
function derivative(x) { return 2 * x; }
// Exact solution: y = x^2 + C
function analytical(x, C) { return x * x + C; }
function toScreen(lx, ly) {
const w = canvas.width;
const h = canvas.height;
const sx = ((lx - LOGIC_X_MIN) / (LOGIC_X_MAX - LOGIC_X_MIN)) * w;
const sy = h - ((ly - LOGIC_Y_MIN) / (LOGIC_Y_MAX - LOGIC_Y_MIN)) * h;
return { x: sx, y: sy };
}
function drawGrid() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 1;
ctx.font = '12px sans-serif';
ctx.fillStyle = '#666';
// Vertical lines (X)
for(let x = 0; x <= LOGIC_X_MAX; x += 1) {
const p = toScreen(x, LOGIC_Y_MIN);
const top = toScreen(x, LOGIC_Y_MAX);
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(top.x, top.y); ctx.stroke();
ctx.fillText(x, p.x + 2, p.y - 5);
}
// Horizontal lines (Y)
for(let y = -10; y <= LOGIC_Y_MAX; y += 10) {
const p = toScreen(LOGIC_X_MIN, y);
const right = toScreen(LOGIC_X_MAX, y);
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(right.x, right.y); ctx.stroke();
ctx.fillText(y, p.x + 2, p.y - 2);
}
// Axes
const origin = toScreen(0, 0);
const xEnd = toScreen(LOGIC_X_MAX, 0);
ctx.strokeStyle = '#333'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(toScreen(LOGIC_X_MIN, 0).x, origin.y); ctx.lineTo(xEnd.x, xEnd.y); ctx.stroke();
const yEnd = toScreen(0, LOGIC_Y_MAX);
ctx.beginPath(); ctx.moveTo(origin.x, toScreen(0, LOGIC_Y_MIN).y); ctx.lineTo(origin.x, yEnd.y); ctx.stroke();
}
function drawExactCurve() {
ctx.beginPath();
ctx.strokeStyle = '#1976d2';
ctx.lineWidth = 2;
ctx.setLineDash([]);
let first = true;
for(let x = LOGIC_X_MIN; x <= LOGIC_X_MAX; x += 0.05) {
const y = analytical(x, state.y0);
const p = toScreen(x, y);
if(first) { ctx.moveTo(p.x, p.y); first = false; }
else { ctx.lineTo(p.x, p.y); }
}
ctx.stroke();
ctx.fillStyle = '#1976d2';
const txtP = toScreen(5.5, analytical(5.5, state.y0));
if (txtP.y > 0 && txtP.y < canvas.height) {
ctx.fillText("Exact", txtP.x - 30, txtP.y - 10);
}
}
function drawHistory() {
if (state.history.length === 0) return;
ctx.lineWidth = 1;
ctx.strokeStyle = '#9e9e9e';
ctx.fillStyle = '#9e9e9e';
state.history.forEach(run => {
if(run.length < 2) return;
ctx.beginPath();
const p0 = toScreen(run[0].x, run[0].y);
ctx.moveTo(p0.x, p0.y);
for(let i=1; i<run.length; i++) {
const p = toScreen(run[i].x, run[i].y);
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
for(let i=0; i<run.length; i++) {
const p = toScreen(run[i].x, run[i].y);
ctx.beginPath();
ctx.arc(p.x, p.y, 2, 0, Math.PI*2);
ctx.fill();
}
});
}
function drawNumericalSoFar() {
if (state.points.length === 0) return;
ctx.beginPath();
ctx.strokeStyle = '#d32f2f';
ctx.lineWidth = 2;
let p0 = toScreen(state.points[0].x, state.points[0].y);
ctx.moveTo(p0.x, p0.y);
const limit = Math.min(state.currentStep, state.points.length - 1);
for(let i = 1; i <= limit; i++) {
const p = toScreen(state.points[i].x, state.points[i].y);
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
ctx.fillStyle = '#d32f2f';
for(let i = 0; i <= limit; i++) {
const p = toScreen(state.points[i].x, state.points[i].y);
ctx.beginPath();
ctx.arc(p.x, p.y, 4, 0, Math.PI*2);
ctx.fill();
}
}
// --- UPDATE MATH PANEL ---
function updateMathPanel(curr, next, slope) {
mathPanel.style.display = 'block';
const yOldStr = curr.y.toFixed(2);
const slopeStr = slope.toFixed(2);
const hStr = state.h.toFixed(1);
const yNewStr = next.y.toFixed(2);
// Uses y' notation and ·
mathCalc.innerHTML = `
y<sub>new</sub> = <span class="val-old">${yOldStr}</span> +
<span class="val-slope">(${slopeStr})</span> ·
<span class="val-h">${hStr}</span> <br>
y<sub>new</sub> = <span class="val-new">${yNewStr}</span>
`;
}
function drawConstructionDetails() {
if (state.currentStep >= state.points.length - 1) return;
const curr = state.points[state.currentStep];
const next = state.points[state.currentStep + 1];
const pCurr = toScreen(curr.x, curr.y);
const pNext = toScreen(next.x, next.y);
const pBase = toScreen(next.x, curr.y);
const slope = derivative(curr.x);
// 1. Update Panel
updateMathPanel(curr, next, slope);
// 2. Construction Triangle
ctx.fillStyle = 'rgba(255, 160, 0, 0.15)';
ctx.beginPath();
ctx.moveTo(pCurr.x, pCurr.y);
ctx.lineTo(pBase.x, pBase.y);
ctx.lineTo(pNext.x, pNext.y);
ctx.fill();
// 3. Tangent Line
ctx.strokeStyle = '#f57f17';
ctx.lineWidth = 3;
ctx.setLineDash([]);
ctx.beginPath();
ctx.moveTo(pCurr.x, pCurr.y);
ctx.lineTo(pNext.x, pNext.y);
ctx.stroke();
// 4. Vertical helper
ctx.strokeStyle = '#f57f17';
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(pNext.x, pNext.y);
ctx.lineTo(pNext.x, pBase.y);
ctx.stroke();
// 5. Horizontal helper
ctx.beginPath();
ctx.moveTo(pCurr.x, pCurr.y);
ctx.lineTo(pBase.x, pBase.y);
ctx.stroke();
ctx.setLineDash([]);
// Text on canvas
ctx.fillStyle = '#ef6c00';
ctx.font = 'bold 12px sans-serif';
if (pCurr.x < canvas.width - 60 && pCurr.y > 30) {
// Using "y'" here as well
ctx.fillText(`y' = ${slope.toFixed(2)}`, pCurr.x + 10, pCurr.y - 15);
}
ctx.fillStyle = '#666';
ctx.font = 'italic 11px sans-serif';
ctx.fillText("h", (pCurr.x + pBase.x)/2, pBase.y + 12);
}
function calculatePoints() {
state.points = [];
let x = 0;
let y = state.y0;
state.points.push({x, y});
while(x < LOGIC_X_MAX) {
const slope = derivative(x);
y = y + slope * state.h;
x = x + state.h;
if (x > LOGIC_X_MAX + state.h) break;
state.points.push({x, y});
}
}
function renderFrame() {
drawGrid();
drawHistory();
drawExactCurve();
drawNumericalSoFar();
if (state.isAnimating) {
drawConstructionDetails();
}
}
function nextAnimationStep() {
if (state.currentStep < state.points.length - 1) {
state.currentStep++;
renderFrame();
state.timer = setTimeout(nextAnimationStep, state.animationSpeed);
} else {
state.isAnimating = false;
btnRestart.disabled = false;
renderFrame();
}
}
function startAnimation(shouldSaveHistory = true) {
if(state.timer) clearTimeout(state.timer);
mathPanel.style.display = 'none';
if (shouldSaveHistory && state.points.length > 0) {
state.history.push([...state.points]);
if(state.history.length > 10) state.history.shift();
}
state.y0 = parseFloat(inputY0.value);
state.h = parseFloat(inputH.value);
const speedVal = parseInt(inputSpeed.value);
state.animationSpeed = 1600 - (speedVal * 150);
if (state.animationSpeed < 50) state.animationSpeed = 50;
calculatePoints();
state.currentStep = 0;
state.isAnimating = true;
btnRestart.disabled = true;
renderFrame();
state.timer = setTimeout(nextAnimationStep, state.animationSpeed);
}
// --- LISTENERS ---
inputY0.addEventListener('input', () => {
dispY0.innerText = parseFloat(inputY0.value).toFixed(1);
});
inputY0.addEventListener('change', () => startAnimation(true));
inputH.addEventListener('change', () => {
dispH.innerText = parseFloat(inputH.value).toFixed(1);
startAnimation(true);
});
inputH.addEventListener('input', () => {
dispH.innerText = parseFloat(inputH.value).toFixed(1);
});
inputSpeed.addEventListener('input', () => {
const speedVal = parseInt(inputSpeed.value);
state.animationSpeed = 1600 - (speedVal * 150);
});
btnRestart.addEventListener('click', () => startAnimation(true));
btnClear.addEventListener('click', () => {
state.history = [];
mathPanel.style.display = 'none';
renderFrame();
});
// Init
dispY0.innerText = parseFloat(inputY0.value).toFixed(1);
dispH.innerText = parseFloat(inputH.value).toFixed(1);
state.y0 = parseFloat(inputY0.value);
calculatePoints();
state.currentStep = 0;
renderFrame();
})();
</script>
</div>
</body>
</html>
```
### Second order differential equations
Let's consider the second order differential equation
$$
\frac{d^2y(x)}{dx^2} = -y(x)
$$
This is a simple differential equation that can be solved by integration. The solution is
$$
y(x) = A \sin(x) + B \cos(x)
$$
where $A$ and $B$ are integration constants. This is the general solution of the differential equation. The solution is not unique because the constants $A$ and $B$ can take any value. The solution is a sinusoidal function. The constants $A$ and $B$ determine the amplitude and phase of the sinusoidal function.
#### Numerical solution
From Taylor's theorem, we know that the second derivative of a function $f(x)$ can be approximated by
$$
f(x + h) = f(x) + h \frac{df(x)}{dx} + \frac{h^2}{2} \frac{d^2f(x)}{dx^2} + \ldots
$$
also
$$
f(x - h) = f(x) - h \frac{df(x)}{dx} + \frac{h^2}{2} \frac{d^2f(x)}{dx^2} + \ldots
$$
adding these two equations we get
$$
f(x + h) + f(x - h) = 2 f(x) + h^2 \frac{d^2f(x)}{dx^2}
$$
so we can approximate the second derivative by
$$
\frac{f(x + h) + f(x - h) - 2 f(x)}{h^2} \approx \frac{d^2f(x)}{dx^2}
$$
Let's use this approximation to solve the differential equation
$$
\frac{d^2y(x)}{dx^2} = -y(x)
$$
We can approximate the second derivative by
$$
\frac{y(x + h) + y(x - h) - 2 y(x)}{h^2} = -y(x)
$$
This equation can be solved for $y(x + h)$
$$
y(x + h) = 2 y(x) - y(x - h) - h^2 y(x)
$$
This equation can be used to solve the differential equation numerically. We can start from an initial value of $y(x)$ and $y(x - h)$ and use the equation above to calculate the value of $y(x + h)$. This value can be used to calculate the next value of $y(x + 2h)$ and so on. This is a simple numerical method to solve differential equations.
Let us compare the numerical solution with the analytical solution.
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Define the second derivative d^2y/dx^2
def d2y_dx2(x):
return -x
# Define the exact analytical solution y(x) for comparison
def y_analytical(x):
return -np.sin(x)
# Define a numerical solution for y(x) using the forward Euler method
def y_numerical(x, h):
y = [0, np.sin(-h)] # Initialize y with the starting values, assuming y(0) = 0 and y(-h) = sin(-h)
for i in range(1, len(x) - 1):
y.append(2 * y[-1] - y[-2] - h**2 * y[-1]) # Update y using d^2y/dx^2
return y
# Set up the x values
plot_x = np.linspace(0, 10, 100)
x = np.linspace(0, 10, 20)
h = x[1] - x[0] # Step size
# Compute the difference between analytical and numerical solutions
difference = y_analytical(x) - np.array(y_numerical(x, h))
# Set up the figure with two subplots
fig, axs = plt.subplots(2,1, figsize=(10,8))
# Left plot: Analytical and numerical solutions
axs[0].plot(plot_x, y_analytical(plot_x), label='Analytical', linestyle='dashed', linewidth=2)
axs[0].scatter(x, y_numerical(x, h), label='Numerical', color='red')
axs[0].set_title('Analytical vs Numerical')
axs[0].set_xlabel('x')
axs[0].set_ylabel('y')
axs[0].legend()
# Right plot: Difference between solutions
axs[1].scatter(x, difference, label='Difference')
axs[1].set_title('Difference (Analytical - Numerical)')
axs[1].set_xlabel('x')
axs[1].set_ylabel('Difference')
axs[1].legend()
# Adjust layout and show the plots
plt.tight_layout()
plt.show()
```
### Html example
```{=html}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Harmonic Oscillator Visualization</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #fff;
display: flex;
justify-content: center;
}
/* Namespace .harmonic-sim to isolate styles */
.harmonic-sim {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0 auto;
padding: 10px;
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 8px;
color: #333;
width: 100%;
max-width: 800px;
box-sizing: border-box;
}
.harmonic-sim .sim-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.harmonic-sim .canvas-wrapper {
position: relative;
width: 100%;
max-width: 700px;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
.harmonic-sim canvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
cursor: default;
}
/* --- Math Panel Styles --- */
.harmonic-sim .math-panel {
position: absolute;
top: 10px;
right: 10px; /* ZMIANA: Panel przeniesiony na prawą stronę */
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #1976d2;
border-left: 5px solid #1976d2;
padding: 10px 15px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 13px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
pointer-events: none;
z-index: 10;
display: none;
line-height: 1.6;
text-align: right; /* Opcjonalnie: wyrównanie tekstu do prawej dla estetyki */
}
.harmonic-sim .math-label {
color: #666;
font-weight: bold;
font-size: 11px;
text-transform: uppercase;
margin-bottom: 2px;
display: block;
border-bottom: 1px solid #eee;
}
.harmonic-sim .math-row {
white-space: nowrap;
}
.harmonic-sim .val-inertia { color: #757575; font-weight: bold; } /* Grey for straight line */
.harmonic-sim .val-force { color: #f57f17; font-weight: bold; } /* Orange for force */
.harmonic-sim .val-new { color: #d32f2f; font-weight: bold; } /* Red for result */
/* Controls */
.harmonic-sim .controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
width: 100%;
padding: 15px;
background: #eee;
border-radius: 6px;
}
.harmonic-sim .control-group {
display: flex;
flex-direction: column;
align-items: center;
min-width: 120px;
}
.harmonic-sim label {
font-size: 0.9em;
font-weight: 600;
margin-bottom: 5px;
}
.harmonic-sim .val-display {
font-family: monospace;
color: #d32f2f;
font-weight: bold;
}
.harmonic-sim .btn-group {
display: flex;
gap: 10px;
align-items: center;
}
.harmonic-sim button {
padding: 8px 16px;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
font-size: 14px;
transition: background 0.2s;
height: 40px;
}
.harmonic-sim #btn-restart { background-color: #1976d2; }
.harmonic-sim #btn-restart:hover { background-color: #1565c0; }
.harmonic-sim #btn-restart:disabled { background-color: #90caf9; cursor: not-allowed; }
.harmonic-sim #btn-clear { background-color: #757575; }
.harmonic-sim #btn-clear:hover { background-color: #616161; }
.harmonic-sim input[type=range] {
cursor: pointer;
}
.harmonic-sim .legend {
font-size: 0.85em;
display: flex;
gap: 15px;
margin-top: 5px;
flex-wrap: wrap;
justify-content: center;
}
.harmonic-sim .legend-item {
display: flex;
align-items: center;
gap: 5px;
}
.harmonic-sim .dot { width: 10px; height: 10px; border-radius: 50%; }
.harmonic-sim .line-sample { width: 20px; height: 3px; border-radius: 2px; }
.harmonic-sim .dashed-sample { width: 20px; height: 2px; border-bottom: 2px dashed #757575; }
</style>
</head>
<body>
<div class="harmonic-sim">
<div class="sim-container">
<div class="controls">
<div class="control-group">
<label>Position y(0): <span id="disp-y0" class="val-display">0.0</span></label>
<input type="range" id="input-y0" min="-5" max="5" step="0.5" value="5">
</div>
<div class="control-group">
<label>Velocity y'(0): <span id="disp-v0" class="val-display">0.0</span></label>
<input type="range" id="input-v0" min="-5" max="5" step="0.5" value="0">
</div>
<div class="control-group">
<label>Step size (h): <span id="disp-h" class="val-display">0.5</span></label>
<input type="range" id="input-h" min="0.1" max="1.5" step="0.1" value="0.5">
</div>
<div class="control-group">
<label>Speed</label>
<input type="range" id="input-speed" min="1" max="10" step="1" value="5">
</div>
<div class="btn-group">
<button id="btn-restart">Start</button>
<button id="btn-clear">Clear</button>
</div>
</div>
<div class="canvas-wrapper">
<canvas id="harmonicCanvas" width="800" height="450"></canvas>
<!-- LIVE MATH PANEL -->
<div id="mathPanel" class="math-panel">
<span class="math-label">2nd Order Step:</span>
<!-- y_new = 2y_n - y_{n-1} - h^2 y_n -->
<div class="math-row">
y<sub>n+1</sub> =
<span class="val-inertia">2y<sub>n</sub> - y<sub>n-1</sub></span>
<span class="val-force">- h<sup>2</sup>y<sub>n</sub></span>
</div>
<div class="math-row" id="mathCalc"></div>
</div>
</div>
<div class="legend">
<div class="legend-item"><div class="line-sample" style="background:#1976d2"></div> Exact (Harmonic)</div>
<div class="legend-item"><div class="dot" style="background:#d32f2f"></div><div class="line-sample" style="background:#d32f2f"></div> Numerical</div>
<div class="legend-item"><div class="dashed-sample"></div> Inertia (No force)</div>
<div class="legend-item"><div class="line-sample" style="background:#f57f17; height: 4px;"></div> Force (-h²y)</div>
<div class="legend-item"><div class="line-sample" style="background:#9e9e9e"></div> History</div>
</div>
</div>
<script>
(function() {
const wrapper = document.querySelector('.harmonic-sim');
const canvas = wrapper.querySelector('#harmonicCanvas');
const ctx = canvas.getContext('2d');
const mathPanel = wrapper.querySelector('#mathPanel');
const mathCalc = wrapper.querySelector('#mathCalc');
const inputY0 = wrapper.querySelector('#input-y0');
const inputV0 = wrapper.querySelector('#input-v0');
const inputH = wrapper.querySelector('#input-h');
const inputSpeed = wrapper.querySelector('#input-speed');
const btnRestart = wrapper.querySelector('#btn-restart');
const btnClear = wrapper.querySelector('#btn-clear');
const dispY0 = wrapper.querySelector('#disp-y0');
const dispV0 = wrapper.querySelector('#disp-v0');
const dispH = wrapper.querySelector('#disp-h');
// Coordinate System Config
const LOGIC_X_MIN = -0.5;
const LOGIC_X_MAX = 20.0;
const LOGIC_Y_MIN = -8;
const LOGIC_Y_MAX = 8;
let state = {
y0: 5,
v0: 0,
h: 0.5,
animationSpeed: 500,
currentStep: 0, // Starts at 0 (index of points array)
isAnimating: false,
points: [], // {x, y}
history: [],
timer: null
};
// Exact solution: y'' = -y => y(x) = A sin(x) + B cos(x)
// y(0) = B
// y'(0) = A
function analytical(x, y0, v0) {
return v0 * Math.sin(x) + y0 * Math.cos(x);
}
function toScreen(lx, ly) {
const w = canvas.width;
const h = canvas.height;
const sx = ((lx - LOGIC_X_MIN) / (LOGIC_X_MAX - LOGIC_X_MIN)) * w;
const sy = h - ((ly - LOGIC_Y_MIN) / (LOGIC_Y_MAX - LOGIC_Y_MIN)) * h;
return { x: sx, y: sy };
}
function drawGrid() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 1;
ctx.font = '12px sans-serif';
ctx.fillStyle = '#666';
// Vertical lines
for(let x = 0; x <= LOGIC_X_MAX; x += 2) {
const p = toScreen(x, LOGIC_Y_MIN);
const top = toScreen(x, LOGIC_Y_MAX);
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(top.x, top.y); ctx.stroke();
ctx.fillText(x, p.x + 2, p.y - 5);
}
// Horizontal lines
for(let y = -5; y <= 5; y += 2.5) {
if(y===0) continue; // Skip axis, draw later
const p = toScreen(LOGIC_X_MIN, y);
const right = toScreen(LOGIC_X_MAX, y);
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(right.x, right.y); ctx.stroke();
ctx.fillText(y, p.x + 2, p.y - 2);
}
// Axes
const origin = toScreen(0, 0);
const xEnd = toScreen(LOGIC_X_MAX, 0);
const yStart = toScreen(0, LOGIC_Y_MAX);
const yEnd = toScreen(0, LOGIC_Y_MIN);
ctx.strokeStyle = '#333'; ctx.lineWidth = 2;
// X Axis
ctx.beginPath(); ctx.moveTo(toScreen(LOGIC_X_MIN, 0).x, origin.y); ctx.lineTo(xEnd.x, xEnd.y); ctx.stroke();
// Y Axis
ctx.beginPath(); ctx.moveTo(origin.x, yStart.y); ctx.lineTo(origin.x, yEnd.y); ctx.stroke();
}
function drawExactCurve() {
ctx.beginPath();
ctx.strokeStyle = '#1976d2';
ctx.lineWidth = 2;
ctx.setLineDash([]);
let first = true;
for(let x = LOGIC_X_MIN; x <= LOGIC_X_MAX; x += 0.1) {
const y = analytical(x, state.y0, state.v0);
const p = toScreen(x, y);
if(first) { ctx.moveTo(p.x, p.y); first = false; }
else { ctx.lineTo(p.x, p.y); }
}
ctx.stroke();
}
function drawHistory() {
if (state.history.length === 0) return;
ctx.lineWidth = 1;
ctx.strokeStyle = '#9e9e9e';
ctx.fillStyle = '#9e9e9e';
state.history.forEach(run => {
if(run.length < 2) return;
ctx.beginPath();
const p0 = toScreen(run[0].x, run[0].y);
ctx.moveTo(p0.x, p0.y);
for(let i=1; i<run.length; i++) {
const p = toScreen(run[i].x, run[i].y);
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
});
}
function drawNumericalSoFar() {
if (state.points.length === 0) return;
ctx.beginPath();
ctx.strokeStyle = '#d32f2f';
ctx.lineWidth = 2;
let p0 = toScreen(state.points[0].x, state.points[0].y);
ctx.moveTo(p0.x, p0.y);
// We draw lines up to currentStep
// Note: points array includes the 'ghost' point at index 0 (x = -h)
// We usually want to visualize starting from x=0 (index 1)
const limit = Math.min(state.currentStep, state.points.length - 1);
// Start drawing from index 1 (x=0) if it exists, otherwise index 0
let startIdx = 1;
if(state.points.length < 2) startIdx = 0;
if(state.points.length > 1) {
let pStart = toScreen(state.points[startIdx].x, state.points[startIdx].y);
ctx.moveTo(pStart.x, pStart.y);
for(let i = startIdx + 1; i <= limit; i++) {
const p = toScreen(state.points[i].x, state.points[i].y);
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
}
ctx.fillStyle = '#d32f2f';
for(let i = startIdx; i <= limit; i++) {
const p = toScreen(state.points[i].x, state.points[i].y);
ctx.beginPath();
ctx.arc(p.x, p.y, 4, 0, Math.PI*2);
ctx.fill();
}
}
function updateMathPanel(curr, prev, next) {
mathPanel.style.display = 'block';
// Formula: y_new = 2*y_n - y_{n-1} - h^2*y_n
// Which is: (Inertia part) + (Force part)
// Inertia: 2*curr - prev
// Force: -h^2 * curr
const inertiaVal = 2 * curr.y - prev.y;
const forceVal = -(state.h * state.h * curr.y);
const nextVal = next.y;
mathCalc.innerHTML = `
y<sub>new</sub> =
<span class="val-inertia">(${inertiaVal.toFixed(2)})</span>
<span class="val-force">+ (${forceVal.toFixed(2)})</span> <br>
y<sub>new</sub> = <span class="val-new">${nextVal.toFixed(2)}</span>
`;
}
function drawConstructionDetails() {
// We need at least 3 points to show the step: prev, curr, next
if (state.currentStep < 1 || state.currentStep >= state.points.length - 1) return;
const idx = state.currentStep;
const prev = state.points[idx - 1]; // y_{n-1}
const curr = state.points[idx]; // y_n
const next = state.points[idx + 1]; // y_{n+1}
const pPrev = toScreen(prev.x, prev.y);
const pCurr = toScreen(curr.x, curr.y);
const pNext = toScreen(next.x, next.y);
// 1. Inertia Point (Project straight line)
// y_inertia = 2*curr - prev = curr + (curr - prev)
const inertiaY = 2 * curr.y - prev.y;
const pInertia = toScreen(next.x, inertiaY);
updateMathPanel(curr, prev, next);
// Draw line extension (Inertia)
ctx.beginPath();
ctx.strokeStyle = '#757575';
ctx.lineWidth = 1;
ctx.setLineDash([5, 5]);
ctx.moveTo(pPrev.x, pPrev.y);
ctx.lineTo(pInertia.x, pInertia.y); // Pass through curr to inertia
ctx.stroke();
// Mark inertia point
ctx.fillStyle = '#757575';
ctx.beginPath(); ctx.arc(pInertia.x, pInertia.y, 3, 0, 6.28); ctx.fill();
// 2. Force Vector (Drop from Inertia to Next)
ctx.beginPath();
ctx.strokeStyle = '#f57f17'; // Orange
ctx.lineWidth = 3;
ctx.setLineDash([]);
ctx.moveTo(pInertia.x, pInertia.y);
ctx.lineTo(pNext.x, pNext.y);
ctx.stroke();
// Arrow head
const headLen = 6;
const angle = Math.atan2(pNext.y - pInertia.y, pNext.x - pInertia.x);
ctx.beginPath();
ctx.moveTo(pNext.x, pNext.y);
ctx.lineTo(pNext.x - headLen * Math.cos(angle - Math.PI/6), pNext.y - headLen * Math.sin(angle - Math.PI/6));
ctx.lineTo(pNext.x - headLen * Math.cos(angle + Math.PI/6), pNext.y - headLen * Math.sin(angle + Math.PI/6));
ctx.fillStyle = '#f57f17';
ctx.fill();
// Text Labels
ctx.font = '11px sans-serif';
ctx.fillStyle = '#757575';
ctx.fillText("Inertia", pInertia.x + 5, pInertia.y);
ctx.fillStyle = '#f57f17';
ctx.fillText("Force (-h²y)", pInertia.x + 5, (pInertia.y + pNext.y)/2);
}
function calculatePoints() {
state.points = [];
// We need y(0) and y(-h) to start.
const x0 = 0;
const y0 = state.y0;
const h = state.h;
// Ghost point at x = -h
const x_ghost = -h;
const y_ghost = y0 * (1 - 0.5 * h * h) - h * state.v0;
state.points.push({x: x_ghost, y: y_ghost}); // Index 0
state.points.push({x: x0, y: y0}); // Index 1 (Start)
let x = x0;
let y_curr = y0;
let y_prev = y_ghost;
while(x < LOGIC_X_MAX) {
// Verlet step: y_next = 2*y_curr - y_prev - h^2 * y_curr
// = (2 - h^2)*y_curr - y_prev
const y_next = (2 - h*h) * y_curr - y_prev;
const x_next = x + h;
state.points.push({x: x_next, y: y_next});
y_prev = y_curr;
y_curr = y_next;
x = x_next;
}
}
function renderFrame() {
drawGrid();
drawHistory();
drawExactCurve();
drawNumericalSoFar();
if (state.isAnimating) {
drawConstructionDetails();
}
}
function nextAnimationStep() {
if (state.currentStep < state.points.length - 2) {
// We stop before the very last point so we can show construction of the last one
// currentStep tracks the "center" point of the verlet stencil
state.currentStep++;
renderFrame();
state.timer = setTimeout(nextAnimationStep, state.animationSpeed);
} else {
state.currentStep++; // Draw final state
state.isAnimating = false;
btnRestart.disabled = false;
renderFrame();
}
}
function startAnimation(shouldSaveHistory = true) {
if(state.timer) clearTimeout(state.timer);
mathPanel.style.display = 'none';
// Points array starts with ghost point (idx 0), then y0 (idx 1).
// We want to save history starting from idx 1 (actual trace)
if (shouldSaveHistory && state.points.length > 1) {
const visibleTrace = state.points.slice(1);
state.history.push(visibleTrace);
if(state.history.length > 8) state.history.shift();
}
state.y0 = parseFloat(inputY0.value);
state.v0 = parseFloat(inputV0.value);
state.h = parseFloat(inputH.value);
const speedVal = parseInt(inputSpeed.value);
state.animationSpeed = 1600 - (speedVal * 150);
if (state.animationSpeed < 50) state.animationSpeed = 50;
calculatePoints();
// Start animation from index 1 (x=0 is established, we show construction of x=h)
state.currentStep = 1;
state.isAnimating = true;
btnRestart.disabled = true;
renderFrame();
state.timer = setTimeout(nextAnimationStep, state.animationSpeed);
}
// --- LISTENERS ---
function updateDisplays() {
dispY0.innerText = parseFloat(inputY0.value).toFixed(1);
dispV0.innerText = parseFloat(inputV0.value).toFixed(1);
dispH.innerText = parseFloat(inputH.value).toFixed(1);
}
// Inputs trigger restart on change
[inputY0, inputV0, inputH].forEach(input => {
input.addEventListener('input', updateDisplays);
input.addEventListener('change', () => startAnimation(true));
});
inputSpeed.addEventListener('input', () => {
const speedVal = parseInt(inputSpeed.value);
state.animationSpeed = 1600 - (speedVal * 150);
});
btnRestart.addEventListener('click', () => startAnimation(true));
btnClear.addEventListener('click', () => {
state.history = [];
mathPanel.style.display = 'none';
renderFrame();
});
// Init
updateDisplays();
state.y0 = parseFloat(inputY0.value);
state.v0 = parseFloat(inputV0.value);
state.h = parseFloat(inputH.value);
calculatePoints();
// Initial render: show up to start point
state.currentStep = 1;
state.isAnimating = false;
renderFrame();
})();
</script>
</div>
</body>
</html>
```
## Gradient
The **gradient** of a scalar field is itself a vector field that points in the direction of the greatest rate of increase of that scalar field. Its magnitude indicates how rapidly the value of the scalar field changes. Formally, for a function $f(x, y, z)$, the gradient is:
$$
\nabla f = \left( \frac{\partial f}{\partial x}, \frac{\partial f}{\partial y}, \frac{\partial f}{\partial z} \right).
$$
If a function $f$ is defined in 2D space as $f(x, y)$, then
$$
\nabla f(x, y) = \left( \frac{\partial f}{\partial x}, \frac{\partial f}{\partial y} \right).
$$
::: warning
Any scalar field $f(x, y)$ has a corresponding gradient field $\nabla f$. **However**, not every vector field is necessarily the gradient of some scalar field.
:::
---
### 1D Example
For a one-dimensional function $f(x) = \sin(x)$ on the interval $[0, 10]$, the gradient (or in this 1D case, simply the derivative) is:
$$
f'(x) = \cos(x).
$$
Below is a Python example that illustrates how to compute these derivatives at a few sample points. We then represent them as small vectors placed at $y = 2$ in a plot, pointing upward if $f'(x)$ is positive and downward if $f'(x)$ is negative, with a scaled magnitude.
```{python}
import numpy as np
import matplotlib.pyplot as plt
# 1D function
def f(x):
return np.sin(x)
# Its derivative (gradient in 1D)
def df(x):
return np.cos(x)
# Sample points on [0, 10]
xs = np.linspace(0, 10, 100)
values_f = f(xs)
# Choose a few points to visualize gradient vectors
sample_points = np.linspace(0, 10, 20)
grad_values = df(sample_points)
fig, ax = plt.subplots(figsize=(8, 4))
# Plot sin(x)
ax.plot(xs, values_f, label='f(x) = sin(x)')
# Plot gradient vectors at y=2 for each sample point
for x_i, grad in zip(sample_points, grad_values):
# We'll scale the arrow length by 0.5 for visibility
ax.arrow(
x_i, f(x_i), grad,0 ,
head_width=0.1, head_length=0.1, length_includes_head=True,
fc='green', ec='red'
)
ax.set_ylim(-1.5, 2)
ax.set_xlabel('x')
ax.set_ylabel('f(x)')
ax.set_title('1D Gradient (Derivative) of sin(x)')
ax.legend()
plt.show()
```
In this plot:
- The blue curve is $\sin(x)$.
- The red arrows at show the derivative $f'(x) = \cos(x)$ at the chosen sample points.
- Arrows pointing up indicate a positive derivative.
---
### 2D Example
Now let’s consider a scalar field in two variables. For instance:
$$
f(x, y) = \sin(x)\cos(y).
$$
Let us compute partial derivatives of this function. Derivative with respect to $x$ is:
$$
\frac{\partial f(x,y)}{\partial x} = \frac{\partial}{\partial x}[\sin(x)\cos(y)] = \cos(x)\cos(y).
$$
Derivative with respect to $y$ is:
$$
\frac{\partial f(x,y)}{\partial y} = \frac{\partial}{\partial y}[\sin(x)\cos(y)] = -\sin(x)\sin(y).
$$
so the gradient of the function $f(x, y)$ is:
$$
\nabla f(x, y) =
\bigl(\cos(x)\cos(y),\; -\sin(x)\sin(y)\bigr).
$$
```{python}
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# Define the 2D scalar field
def f2d(x, y):
return np.sin(x) * np.cos(y)
# Create a grid for plotting
nx, ny = 60, 60
x_vals = np.linspace(0, 2*np.pi, nx)
y_vals = np.linspace(0, 2*np.pi, ny)
X, Y = np.meshgrid(x_vals, y_vals)
Z = f2d(X, Y)
# --- 3D Surface Plot ---
fig3d = plt.figure(figsize=(8, 6))
ax3d = fig3d.add_subplot(111, projection='3d')
# Create a surface plot
surf = ax3d.plot_surface(
X, Y, Z,
cmap='coolwarm',
edgecolor='none',
alpha=0.9
)
fig3d.colorbar(surf, ax=ax3d, shrink=0.6, label='f(x,y) = sin(x)*cos(y)')
ax3d.set_xlabel('x')
ax3d.set_ylabel('y')
# ax3d.set_zlabel('f(x,y)')
ax3d.set_title('3D Surface of f(x, y) = sin(x)*cos(y)')
plt.show()
```
Below is a Python example that:
1. Plots a heatmap of $f(x, y)$.
2. Overlays gradient vectors (arrows) on a grid of points.
```{python}
import numpy as np
import matplotlib.pyplot as plt
# Define the 2D scalar field
def f2d(x, y):
return np.sin(x) * np.cos(y)
# Define partial derivatives (gradient)
def grad_f2d(x, y):
# df/dx = cos(x)*cos(y)
# df/dy = -sin(x)*sin(y)
return np.cos(x)*np.cos(y), -2*np.sin(x)*np.sin(y)
# Create a grid for plotting
nx, ny = 60, 60
x_vals = np.linspace(0, 2*np.pi, nx)
y_vals = np.linspace(0, 2*np.pi, ny)
X, Y = np.meshgrid(x_vals, y_vals)
Z = f2d(X, Y)
# Compute gradient on a coarser grid for quiver plot
skip = 3
x_quiv = X[::skip, ::skip]
y_quiv = Y[::skip, ::skip]
Fx, Fy = grad_f2d(x_quiv, y_quiv)
fig, ax = plt.subplots(figsize=(8, 6))
# Heatmap of f(x,y)
c = ax.imshow(
Z,
extent=[x_vals.min(), x_vals.max(), y_vals.min(), y_vals.max()],
origin='lower',
cmap='coolwarm'
)
fig.colorbar(c, ax=ax, label='f(x,y) = sin(x)*cos(y)')
# Quiver plot of gradient vectors
ax.quiver(
x_quiv, y_quiv, Fx, Fy,
color='black',
pivot='mid',
alpha=0.8,
width=0.003,
scale=50
)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title('Gradient of f(x, y) = sin(x)*cos(y)')
plt.show()
```
**Interpretation**:
- The **heatmap** shows the values of $f(x,y)$. Red/blue corresponds to high/low values of the scalar field.
- The **arrows** show the local direction and magnitude of the gradient, i.e., where $f(x, y)$ increases the most and how quickly.
This demonstrates how any scalar field (like temperature, potential, or pressure) naturally defines a vector field via its gradient. However, **not every vector field arises as a gradient** of a scalar field—certain mathematical conditions (like having zero curl in a simply connected domain) must be satisfied for a vector field to be the gradient of some scalar field.
#### Html example
```{=html}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gradient Descent Explorer 1D</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #fff;
display: flex;
justify-content: center;
}
/* Namespace .gd-1d-sim */
.gd-1d-sim {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0 auto;
background-color: #fcfcfc;
border: 1px solid #ddd;
border-radius: 8px;
color: #333;
width: 100%;
max-width: 900px;
box-sizing: border-box;
display: flex;
flex-direction: column;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
overflow: hidden;
}
.gd-1d-sim header {
background-color: #fff;
padding: 15px 20px;
border-bottom: 1px solid #eee;
}
.gd-1d-sim h3 {
margin: 0;
color: #2c3e50;
font-size: 1.2rem;
}
.gd-1d-sim .main-content {
display: flex;
flex-direction: column;
position: relative;
}
.gd-1d-sim canvas {
display: block;
background: #ffffff;
cursor: crosshair;
width: 100%;
height: 400px; /* Fixed height for canvas logic */
}
/* CONTROLS & MATH CONTAINER */
.gd-1d-sim .controls {
padding: 20px;
background: #f1f3f4;
border-top: 1px solid #ddd;
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: center;
justify-content: center;
}
/* NEW MATH PANEL STYLE (Embedded in controls) */
.gd-1d-sim .math-panel {
width: 100%; /* Take full width of the controls container */
background: #fff;
border: 1px solid #ccc;
border-left: 4px solid #d32f2f;
padding: 15px 20px;
border-radius: 4px;
margin-bottom: 5px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
font-family: 'Courier New', monospace;
font-size: 0.95rem;
display: none; /* Hidden by default */
}
.gd-1d-sim .math-row {
margin-bottom: 8px;
}
.gd-1d-sim .math-title {
font-family: sans-serif;
font-weight: bold;
color: #555;
font-size: 0.8rem;
margin-bottom: 8px;
display: block;
text-transform: uppercase;
border-bottom: 1px solid #eee;
padding-bottom: 4px;
}
.gd-1d-sim .val-x { color: #1976d2; font-weight: bold; }
.gd-1d-sim .val-slope { color: #d32f2f; font-weight: bold; }
.gd-1d-sim .val-lr { color: #2e7d32; font-weight: bold; }
.gd-1d-sim .val-new { color: #333; font-weight: bold; background: #eee; padding: 0 4px; }
/* INPUTS & BUTTONS */
.gd-1d-sim .control-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.gd-1d-sim label {
font-weight: 600;
font-size: 0.85rem;
color: #555;
}
.gd-1d-sim input[type=range] {
cursor: pointer;
width: 150px;
}
.gd-1d-sim button {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
font-size: 14px;
transition: background 0.2s, transform 0.1s;
}
.gd-1d-sim .btn-step {
background-color: #f57c00;
color: white;
min-width: 120px;
}
.gd-1d-sim .btn-step:hover { background-color: #ef6c00; }
.gd-1d-sim .btn-step:active { transform: translateY(1px); }
.gd-1d-sim .btn-run {
background-color: #2e7d32;
color: white;
min-width: 100px;
}
.gd-1d-sim .btn-run:hover { background-color: #1b5e20; }
.gd-1d-sim .btn-reset {
background-color: #607d8b;
color: white;
}
.gd-1d-sim .btn-reset:hover { background-color: #455a64; }
.gd-1d-sim .instructions {
margin-top: 10px;
font-size: 0.9rem;
color: #666;
text-align: center;
width: 100%;
}
</style>
</head>
<body>
<div class="gd-1d-sim">
<header>
<h3>Gradient Descent: Finding the Minimum</h3>
</header>
<div class="main-content">
<canvas id="gdCanvas" width="900" height="400"></canvas>
</div>
<div class="controls">
<!-- MATH PANEL MOVED HERE -->
<div class="math-panel" id="mathBox">
<span class="math-title">Current Calculation</span>
<div style="display: flex; flex-wrap: wrap; gap: 20px; align-items: center;">
<div class="math-row">
Slope (f'(x)) = <span id="disp-slope" class="val-slope">0.00</span>
</div>
<div class="math-row">
x<sub>new</sub> = <span id="disp-x-old" class="val-x">0.00</span> - (<span id="disp-lr" class="val-lr">0.1</span> · <span class="val-slope">Slope</span>)
</div>
<div class="math-row">
→ x<sub>new</sub> = <span id="disp-x-new" class="val-new">0.00</span>
</div>
</div>
<div style="font-size: 0.8em; color: #777; margin-top:5px;">
<em>Ball rolls opposite to the slope (Gradient Descent Step).</em>
</div>
</div>
<div class="control-group">
<label>Learning Rate (Step Size)</label>
<input type="range" id="lrSlider" min="0.01" max="1.0" step="0.01" value="0.2">
<span style="font-size: 0.8rem; color:#555; text-align: center;" id="lrVal">0.20</span>
</div>
<button class="btn-step" id="btnStep">Take 1 Step</button>
<button class="btn-run" id="btnRun">Auto Run</button>
<button class="btn-reset" id="btnReset">Clear</button>
<div class="instructions">
<strong>Click anywhere on the curve</strong> to place the ball, then use controls to descend.
</div>
</div>
<script>
(function() {
const canvas = document.getElementById('gdCanvas');
const ctx = canvas.getContext('2d');
const mathBox = document.getElementById('mathBox');
// Displays
const dispSlope = document.getElementById('disp-slope');
const dispXOld = document.getElementById('disp-x-old');
const dispLr = document.getElementById('disp-lr');
const dispXNew = document.getElementById('disp-x-new');
const lrSlider = document.getElementById('lrSlider');
const lrVal = document.getElementById('lrVal');
// Buttons
const btnStep = document.getElementById('btnStep');
const btnRun = document.getElementById('btnRun');
const btnReset = document.getElementById('btnReset');
// Simulation State
const LOGIC_X_MIN = -4;
const LOGIC_X_MAX = 4;
// Y range determined dynamically or fixed
const LOGIC_Y_MIN = -2;
const LOGIC_Y_MAX = 6;
let ball = {
x: null, // Logic X
exists: false,
path: [] // History of points
};
let isRunning = false;
let animationFrame = null;
// --- THE FUNCTION ---
// f(x) = 0.3*x^2 + cos(3*x) + 0.5*x
function f(x) {
return 0.3 * x * x - Math.cos(2 * x) + 0.5 * Math.sin(x) + 1.5;
}
// Derivative f'(x)
function df(x) {
return 0.6 * x + 2 * Math.sin(2 * x) + 0.5 * Math.cos(x);
}
// --- Coordinates ---
function toScreen(lx, ly) {
const w = canvas.width;
const h = canvas.height;
const padding = 40;
const sx = padding + ((lx - LOGIC_X_MIN) / (LOGIC_X_MAX - LOGIC_X_MIN)) * (w - 2*padding);
const sy = h - padding - ((ly - LOGIC_Y_MIN) / (LOGIC_Y_MAX - LOGIC_Y_MIN)) * (h - 2*padding);
return { x: sx, y: sy };
}
function toLogic(sx, sy) {
const w = canvas.width;
const h = canvas.height;
const padding = 40;
const lx = LOGIC_X_MIN + ((sx - padding) / (w - 2*padding)) * (LOGIC_X_MAX - LOGIC_X_MIN);
return lx;
}
// --- Drawing ---
function drawCurve() {
ctx.beginPath();
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
let first = true;
const step = (LOGIC_X_MAX - LOGIC_X_MIN) / 400;
for(let x = LOGIC_X_MIN; x <= LOGIC_X_MAX; x += step) {
const y = f(x);
const p = toScreen(x, y);
if (first) { ctx.moveTo(p.x, p.y); first = false; }
else { ctx.lineTo(p.x, p.y); }
}
ctx.stroke();
}
function drawGrid() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const origin = toScreen(0, 0);
ctx.beginPath();
ctx.strokeStyle = '#eee';
ctx.lineWidth = 1;
// Y Axis
ctx.moveTo(origin.x, 0);
ctx.lineTo(origin.x, canvas.height);
// X Axis
const y0 = toScreen(0, 0).y;
ctx.moveTo(0, y0);
ctx.lineTo(canvas.width, y0);
ctx.stroke();
}
function drawBall() {
if (!ball.exists) return;
const y = f(ball.x);
const p = toScreen(ball.x, y);
const slope = df(ball.x);
// 1. Draw Tangent Line (Red)
const tanLen = 0.8;
const x1 = ball.x - tanLen;
const y1 = slope * (x1 - ball.x) + y;
const x2 = ball.x + tanLen;
const y2 = slope * (x2 - ball.x) + y;
const p1 = toScreen(x1, y1);
const p2 = toScreen(x2, y2);
ctx.beginPath();
ctx.strokeStyle = '#d32f2f';
ctx.lineWidth = 2;
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
// 2. Draw Movement Vector (Green Arrow)
const lr = parseFloat(lrSlider.value);
const stepX = -lr * slope;
const visScale = 1.0;
const vecEnd = toScreen(ball.x + stepX * visScale, y);
ctx.beginPath();
ctx.strokeStyle = '#2e7d32';
ctx.lineWidth = 4;
ctx.moveTo(p.x, p.y);
ctx.lineTo(vecEnd.x, vecEnd.y);
ctx.stroke();
// Arrowhead
const angle = stepX > 0 ? 0 : Math.PI;
ctx.beginPath();
ctx.fillStyle = '#2e7d32';
ctx.moveTo(vecEnd.x, vecEnd.y);
if (stepX > 0) {
ctx.lineTo(vecEnd.x - 8, vecEnd.y - 5);
ctx.lineTo(vecEnd.x - 8, vecEnd.y + 5);
} else {
ctx.lineTo(vecEnd.x + 8, vecEnd.y - 5);
ctx.lineTo(vecEnd.x + 8, vecEnd.y + 5);
}
ctx.fill();
// 3. Draw Path (Ghost trails)
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
for(let pt of ball.path) {
const pp = toScreen(pt.x, f(pt.x));
ctx.beginPath();
ctx.arc(pp.x, pp.y, 3, 0, Math.PI*2);
ctx.fill();
}
// 4. Draw Ball
ctx.beginPath();
ctx.fillStyle = '#d32f2f';
ctx.arc(p.x, p.y, 8, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
// Update Math Panel
updateMath(ball.x, slope, stepX);
}
function updateMath(x, slope, step) {
mathBox.style.display = 'block';
dispXOld.innerText = x.toFixed(3);
dispSlope.innerText = slope.toFixed(3);
dispLr.innerText = parseFloat(lrSlider.value).toFixed(2);
// x_new = x + step
const xNew = x + step;
dispXNew.innerText = xNew.toFixed(3);
}
function render() {
drawGrid();
drawCurve();
drawBall();
}
// --- Algorithms ---
function calculateStep() {
if (!ball.exists) return;
const lr = parseFloat(lrSlider.value);
const slope = df(ball.x);
ball.path.push({x: ball.x});
const step = -lr * slope;
ball.x += step;
// Boundary checks
if (ball.x < LOGIC_X_MIN) ball.x = LOGIC_X_MIN;
if (ball.x > LOGIC_X_MAX) ball.x = LOGIC_X_MAX;
render();
// Check convergence
if (Math.abs(slope) < 0.001) {
return false;
}
return true;
}
function runAnimation() {
if (!isRunning) return;
const keepGoing = calculateStep();
if (keepGoing) {
animationFrame = requestAnimationFrame(() => {
setTimeout(runAnimation, 100);
});
} else {
isRunning = false;
btnRun.innerText = "Auto Run";
}
}
// --- Interaction ---
canvas.addEventListener('mousedown', (e) => {
isRunning = false;
cancelAnimationFrame(animationFrame);
btnRun.innerText = "Auto Run";
const rect = canvas.getBoundingClientRect();
const lx = toLogic(e.clientX - rect.left, e.clientY - rect.top);
ball.x = lx;
ball.exists = true;
ball.path = [];
render();
});
btnStep.addEventListener('click', () => {
if (!ball.exists) {
alert("Click on the graph first to place the ball!");
return;
}
isRunning = false;
calculateStep();
});
btnRun.addEventListener('click', () => {
if (!ball.exists) {
alert("Click on the graph first to place the ball!");
return;
}
if (isRunning) {
isRunning = false;
cancelAnimationFrame(animationFrame);
btnRun.innerText = "Auto Run";
} else {
isRunning = true;
btnRun.innerText = "Stop";
runAnimation();
}
});
btnReset.addEventListener('click', () => {
ball.exists = false;
ball.path = [];
mathBox.style.display = 'none';
isRunning = false;
btnRun.innerText = "Auto Run";
render();
});
lrSlider.addEventListener('input', () => {
lrVal.innerText = parseFloat(lrSlider.value).toFixed(2);
if (ball.exists) render();
});
// Init
render();
})();
</script>
</div>
</body>
</html>
```